chromium/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/RecentlyClosedBridgeTest.java

// Copyright 2022 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.ntp;

import static org.mockito.Mockito.when;

import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CommandLineFlags;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Features.EnableFeatures;
import org.chromium.chrome.browser.ChromeTabbedActivity;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.flags.ChromeSwitches;
import org.chromium.chrome.browser.profiles.ProfileManager;
import org.chromium.chrome.browser.sync.SyncServiceFactory;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab.TabState;
import org.chromium.chrome.browser.tab.TabStateExtractor;
import org.chromium.chrome.browser.tabmodel.TabClosureParams;
import org.chromium.chrome.browser.tabmodel.TabList;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.ChromeTabbedActivityTestRule;
import org.chromium.chrome.test.batch.BlankCTATabInitialStateRule;
import org.chromium.chrome.test.util.ChromeTabUtils;
import org.chromium.components.sync.DataType;
import org.chromium.components.sync.SyncService;
import org.chromium.ui.mojom.WindowOpenDisposition;

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

/** Tests for {@link RecentlyClosedBridge} including native TabRestoreService. */
@RunWith(ChromeJUnit4ClassRunner.class)
@CommandLineFlags.Add({
    ChromeSwitches.DISABLE_FIRST_RUN_EXPERIENCE,
    ChromeSwitches.DISABLE_STARTUP_PROMOS
})
@Batch(Batch.PER_CLASS)
public class RecentlyClosedBridgeTest {
    private static final int MAX_ENTRY_COUNT = 5;
    private static final String TEST_PAGE_A = "/chrome/test/data/android/about.html";
    private static final String TEST_PAGE_B = "/chrome/test/data/android/google.html";
    private static final String TEST_PAGE_C = "/chrome/test/data/android/simple.html";

    @ClassRule
    public static ChromeTabbedActivityTestRule sActivityTestRule =
            new ChromeTabbedActivityTestRule();

    @Rule
    public BlankCTATabInitialStateRule mBlankCTATabInitialStateRule =
            new BlankCTATabInitialStateRule(sActivityTestRule, true);

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

    private ChromeTabbedActivity mActivity;
    private TabModelSelector mTabModelSelector;
    private TabGroupModelFilter mTabGroupModelFilter;
    private TabModel mTabModel;
    private RecentlyClosedBridge mRecentlyClosedBridge;
    @Mock private SyncService mSyncService;

    @Before
    public void setUp() {
        when(mSyncService.getActiveDataTypes()).thenReturn(Set.of(DataType.SAVED_TAB_GROUP));
        SyncServiceFactory.setInstanceForTesting(mSyncService);

        sActivityTestRule.waitForActivityNativeInitializationComplete();

        // Disable snackbars from the {@link UndoBarController} which can break this test.
        sActivityTestRule.getActivity().getSnackbarManager().disableForTesting();

        mActivity = sActivityTestRule.getActivity();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge =
                            new RecentlyClosedBridge(
                                    ProfileManager.getLastUsedRegularProfile(),
                                    mActivity.getTabModelSelectorSupplier().get());
                    mRecentlyClosedBridge.clearRecentlyClosedEntries();
                    Assert.assertEquals(
                            0,
                            mRecentlyClosedBridge.getRecentlyClosedEntries(MAX_ENTRY_COUNT).size());
                });
        mActivity = sActivityTestRule.getActivity();
        mTabModelSelector = mActivity.getTabModelSelectorSupplier().get();
        mTabModel = mTabModelSelector.getModel(false);
        TabModelFilter filter =
                mTabModelSelector.getTabModelFilterProvider().getTabModelFilter(false);
        mTabGroupModelFilter = (TabGroupModelFilter) filter;
        final Tab tab = mActivity.getActivityTab();
        ChromeTabUtils.waitForInteractable(tab);
    }

    @After
    public void tearDown() {
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.clearRecentlyClosedEntries();
                    Assert.assertEquals(
                            0,
                            mRecentlyClosedBridge.getRecentlyClosedEntries(MAX_ENTRY_COUNT).size());
                    mRecentlyClosedBridge.destroy();
                });
    }

    /** Tests opening the most recently closed tab in the foreground. */
    @Test
    @MediumTest
    public void testOpenMostRecentlyClosedEntry_Tab_InForeground() {
        final String[] urls = new String[] {getUrl(TEST_PAGE_A), getUrl(TEST_PAGE_B)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    titles[0] = tabA.getTitle();
                    titles[1] = tabB.getTitle();
                    mTabModel.closeTabs(TabClosureParams.closeTab(tabB).allowUndo(false).build());
                    mTabModel.closeTabs(TabClosureParams.closeTab(tabA).build());
                    mTabModel.commitTabClosure(tabA.getId());
                });

        final List<RecentlyClosedTab> recentTabs = new ArrayList<>();
        final int[] tabCount = new int[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tabCount[0] = mTabModel.getCount();
                    recentTabs.addAll(
                            (List<RecentlyClosedTab>)
                                    (List<? extends RecentlyClosedEntry>)
                                            mRecentlyClosedBridge.getRecentlyClosedEntries(
                                                    MAX_ENTRY_COUNT));
                    mRecentlyClosedBridge.openMostRecentlyClosedEntry(mTabModel);
                });
        // 1. Blank Tab
        Assert.assertEquals(1, tabCount[0]);

        assertTabsAre(recentTabs, titles, urls);

        // 1. Blank Tab
        // 2. tabA - restored.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(2, tabs.size());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // tabA is launched in foreground.
                    Assert.assertNotNull(tabs.get(1).getWebContents().getRenderWidgetHostView());
                });
    }

    /** Tests opening a specific closed {@link Tab} as a new background tab. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedTab_InCurrentTab() {
        final String[] urls = new String[] {getUrl(TEST_PAGE_A), getUrl(TEST_PAGE_B)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabC =
                sActivityTestRule.loadUrlInNewTab(getUrl(TEST_PAGE_C), /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabModel.setIndex(mTabModel.indexOf(tabC), TabSelectionType.FROM_USER);
                    titles[0] = tabA.getTitle();
                    titles[1] = tabB.getTitle();
                    mTabModel.closeTabs(TabClosureParams.closeTab(tabB).allowUndo(false).build());
                    mTabModel.closeTabs(TabClosureParams.closeTab(tabA).allowUndo(false).build());
                });

        final List<RecentlyClosedTab> recentTabs = new ArrayList<>();
        final int[] tabCount = new int[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tabCount[0] = mTabModel.getCount();
                    recentTabs.addAll(
                            (List<RecentlyClosedTab>)
                                    (List<? extends RecentlyClosedEntry>)
                                            mRecentlyClosedBridge.getRecentlyClosedEntries(
                                                    MAX_ENTRY_COUNT));
                    mRecentlyClosedBridge.openRecentlyClosedTab(
                            mTabModel, recentTabs.get(1), WindowOpenDisposition.CURRENT_TAB);
                });
        // 1. Blank Tab
        // 2. tabC
        Assert.assertEquals(2, recentTabs.size());

        assertTabsAre(recentTabs, titles, urls);

        // 1. Blank Tab
        // 2. tabC - now TEST_PAGE_B.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(2, tabs.size());
        // Restored onto tab B.
        Assert.assertEquals(tabC, tabs.get(1));
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabC));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabC).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertNotNull(tabC.getWebContents());
                    // Should only have one navigation entry as it replaced TEST_PAGE_C.
                    Assert.assertEquals(
                            1,
                            tabC.getWebContents()
                                    .getNavigationController()
                                    .getNavigationHistory()
                                    .getEntryCount());

                    // Has renderer for foreground tab.
                    Assert.assertNotNull(tabC.getWebContents().getRenderWidgetHostView());
                });
    }

    /** Tests opening a specific closed {@link Tab} that was frozen as a new background tab. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedTab_Frozen_InBackground() {
        final String[] urls = new String[] {getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);
        sActivityTestRule.loadUrlInNewTab(getUrl(TEST_PAGE_B), /* incognito= */ false);
        final Tab frozenTabA = freezeTab(tabA);
        // Clear the entry created by freezing the tab.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.clearRecentlyClosedEntries();
                });

        final String[] titles = new String[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    titles[0] = frozenTabA.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTab(frozenTabA).allowUndo(false).build());
                });

        final List<RecentlyClosedTab> recentTabs = new ArrayList<>();
        final int[] tabCount = new int[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    tabCount[0] = mTabModel.getCount();
                    recentTabs.addAll(
                            (List<RecentlyClosedTab>)
                                    (List<? extends RecentlyClosedEntry>)
                                            mRecentlyClosedBridge.getRecentlyClosedEntries(
                                                    MAX_ENTRY_COUNT));
                    mRecentlyClosedBridge.openRecentlyClosedTab(
                            mTabModel, recentTabs.get(0), WindowOpenDisposition.NEW_BACKGROUND_TAB);
                });
        // 1. Blank Tab
        // 2. tabB
        Assert.assertEquals(2, tabCount[0]);

        assertTabsAre(recentTabs, titles, urls);

        // 1. Blank Tab
        // 2. tabB
        // 3. tabA - restored.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(3, tabs.size());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(2)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(2)).getSpec());
    }

    /**
     * Tests opening a specific closed {@link Tab} that was closed as part of a bulk closure
     * replacing the current tab.
     */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedTab_FromBulkClosure_InNewTab() {
        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls = new String[] {getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    titles[1] = tabA.getTitle();
                    titles[0] = tabB.getTitle();
                    mTabModel.closeTabs(TabClosureParams.closeAllTabs().build());
                    mTabModel.commitAllTabClosures();
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(0, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0), RecentlyClosedBulkEvent.class, new String[0], titles, urls);

        final RecentlyClosedBulkEvent event = (RecentlyClosedBulkEvent) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedTab(
                            mTabModel,
                            event.getTabs().get(1),
                            WindowOpenDisposition.NEW_FOREGROUND_TAB);
                });

        // 1. tabA - new restored.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(1, tabs.size());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(0)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(0)).getSpec());
    }

    /**
     * Tests opening a specific closed {@link Tab} that was closed as part of a group in a bulk
     * closure as a new background tab.
     */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedTab_FromGroupInBulkClosure_InBackgroundTab() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls =
                new String[] {getUrl(TEST_PAGE_C), getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[2], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabC = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[3];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    titles[2] = tabA.getTitle();
                    titles[1] = tabB.getTitle();
                    titles[0] = tabC.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA, tabB, tabC))
                                    .hideTabGroups(true)
                                    .build());
                    mTabModel.commitAllTabClosures();
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0),
                RecentlyClosedBulkEvent.class,
                new String[] {""},
                titles,
                urls);

        final RecentlyClosedBulkEvent event = (RecentlyClosedBulkEvent) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Tab order is inverted as most recent comes first so pick the last tab to
                    // restore ==
                    // tabB.
                    mRecentlyClosedBridge.openRecentlyClosedTab(
                            mTabModel,
                            event.getTabs().get(1),
                            WindowOpenDisposition.NEW_BACKGROUND_TAB);
                });

        // 1. Blank tab
        // 2. Restored tabB
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(2, tabs.size());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                });
    }

    /**
     * Tests opening a specific closed {@link Tab} that was closed as part of a group replacing the
     * current tab.
     */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedTab_FromGroupClosure_InCurrentTab() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in when closing.
        final String[] urls = new String[] {getUrl(TEST_PAGE_A), getUrl(TEST_PAGE_B)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    titles[0] = tabA.getTitle();
                    titles[1] = tabB.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabB, tabA))
                                    .allowUndo(false)
                                    .hideTabGroups(true)
                                    .build());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0), RecentlyClosedGroup.class, new String[] {""}, titles, urls);

        final RecentlyClosedGroup group = (RecentlyClosedGroup) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedTab(
                            mTabModel, group.getTabs().get(1), WindowOpenDisposition.CURRENT_TAB);
                });

        // 1. tabA restored over blank tab.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(1, tabs.size());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(0)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(0)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(0)));
                });

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedTab(
                            mTabModel,
                            group.getTabs().get(0),
                            WindowOpenDisposition.NEW_BACKGROUND_TAB);
                });

        // 1. tabA restored over blank tab.
        // 2. tabB restored.
        tabs.clear();
        tabs.addAll(getAllTabs());
        Assert.assertEquals(2, tabs.size());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                });
    }

    /** Tests opening a specific closed {@link Tab} that was closed not as the most recent entry. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedEntry_Tab_FromMultipleTabs_SingleTabGroup() {
        if (mTabGroupModelFilter == null) return;

        final String[] urlA = new String[] {getUrl(TEST_PAGE_A)};
        final String[] urlB = new String[] {getUrl(TEST_PAGE_B)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urlA[0], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urlB[0], /* incognito= */ false);

        final String[] titleA = new String[1];
        final String[] titleB = new String[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    titleA[0] = tabA.getTitle();
                    titleB[0] = tabB.getTitle();
                    mTabModel.closeTabs(TabClosureParams.closeTab(tabB).build());
                    mTabModel.closeTabs(TabClosureParams.closeTab(tabA).build());
                    mTabModel.commitAllTabClosures();
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(2, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0), RecentlyClosedGroup.class, new String[] {""}, titleA, urlA);
        assertEntryIs(recentEntries.get(1), RecentlyClosedTab.class, new String[0], titleB, urlB);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedEntry(mTabModel, recentEntries.get(1));
                });

        // 1. Blank tab
        // 2. tabB restored in new tab.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(2, tabs.size());
        Assert.assertEquals(titleB[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urlB[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                });

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedEntry(mTabModel, recentEntries.get(0));
                });

        // 1. Blank tab
        // 2. tabB restored in new tab.
        // 3. tabA restored in new tab as group.
        tabs.clear();
        tabs.addAll(getAllTabs());
        Assert.assertEquals(3, tabs.size());
        Assert.assertEquals(titleA[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(2)));
        Assert.assertEquals(urlA[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(2)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(2)));
                });
    }

    /**
     * Tests opening a specific closed {@link Tab} that was closed as part of a group. There is no
     * UI that currently facilitates this flow. A Group or Bulk closure is either all restored or
     * not.
     */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedEntry_Tab_FromGroupClosure() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls = new String[] {getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    titles[1] = tabA.getTitle();
                    titles[0] = tabB.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA, tabB))
                                    .hideTabGroups(true)
                                    .build());
                    mTabModel.commitTabClosure(tabA.getId());
                    mTabModel.commitTabClosure(tabB.getId());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0), RecentlyClosedGroup.class, new String[] {""}, titles, urls);

        final RecentlyClosedGroup group = (RecentlyClosedGroup) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Tab order is inverted as most recent comes first so pick the last tab to
                    // restore == tabA.
                    mRecentlyClosedBridge.openRecentlyClosedEntry(
                            mTabModel, group.getTabs().get(1));
                });

        // 1. Blank tab
        // 2. tabA restored in new tab in a group.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(2, tabs.size());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());

        // This behavior mirrors desktop.
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                });
    }

    /** Tests opening a specific closed {@link Tab} that was closed as part of a group. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedEntry_SingleRemainingTabInGroupAsGroup_FromGroupClosure() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls = new String[] {getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    titles[1] = tabA.getTitle();
                    titles[0] = tabB.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA, tabB))
                                    .hideTabGroups(true)
                                    .build());
                    mTabModel.commitTabClosure(tabA.getId());
                    mTabModel.commitTabClosure(tabB.getId());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0), RecentlyClosedGroup.class, new String[] {""}, titles, urls);

        final RecentlyClosedGroup group = (RecentlyClosedGroup) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Tab order is inverted as most recent comes first so pick the last tab to
                    // restore ==
                    // tabA.
                    mRecentlyClosedBridge.openRecentlyClosedEntry(
                            mTabModel, group.getTabs().get(1));
                });

        // 1. Blank tab
        // 2. tabA restored in new tab.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(2, tabs.size());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                });
    }

    /** Tests opening a specific closed group. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedEntry_Group_FromGroupClosure() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls = new String[] {getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    mTabGroupModelFilter.setTabGroupTitle(tabA.getId(), "Bar");
                    titles[1] = tabA.getTitle();
                    titles[0] = tabB.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA, tabB))
                                    .allowUndo(false)
                                    .hideTabGroups(true)
                                    .build());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0),
                RecentlyClosedGroup.class,
                new String[] {"Bar"},
                titles,
                urls);

        final RecentlyClosedGroup group = (RecentlyClosedGroup) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedEntry(mTabModel, group);
                });

        // 1. Blank tab
        // 2. tabA restored in new tab.
        // 3. tabB restored in new tab.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(3, tabs.size());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(2)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(2)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertEquals(
                            "Bar", mTabGroupModelFilter.getTabGroupTitle(tabs.get(1).getId()));
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                    Assert.assertEquals(
                            Arrays.asList(new Tab[] {tabs.get(1), tabs.get(2)}),
                            mTabGroupModelFilter.getRelatedTabList(tabs.get(1).getId()));
                });
    }

    /** Tests opening a tabs that are a subset of a group. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedEntry_SubsetOfTabs_FromGroupSubsetClosure_NotUndoable() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls = new String[] {getUrl(TEST_PAGE_C), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabB =
                sActivityTestRule.loadUrlInNewTab(getUrl(TEST_PAGE_B), /* incognito= */ false);
        final Tab tabC = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    mTabGroupModelFilter.mergeTabsToGroup(tabC.getId(), tabA.getId());
                    mTabGroupModelFilter.setTabGroupTitle(tabA.getId(), "Bar");
                    titles[1] = tabA.getTitle();
                    titles[0] = tabC.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA, tabC))
                                    .allowUndo(false)
                                    .hideTabGroups(true)
                                    .build());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(2, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0), RecentlyClosedBulkEvent.class, new String[] {}, titles, urls);

        final RecentlyClosedBulkEvent bulkEvent = (RecentlyClosedBulkEvent) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedEntry(mTabModel, bulkEvent);
                });

        // 1. Blank tab
        // 2. tabB
        // 3. tabA restored in new tab.
        // 4. tabC restored in new tab.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(4, tabs.size());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(2)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(2)).getSpec());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(3)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(3)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(2)));
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(3)));
                });
    }

    /** Tests opening a tabs that are a subset of a group. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedEntry_SubsetOfTabs_FromGroupSubsetClosure_Undoable() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls = new String[] {getUrl(TEST_PAGE_C), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabB =
                sActivityTestRule.loadUrlInNewTab(getUrl(TEST_PAGE_B), /* incognito= */ false);
        final Tab tabC = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    mTabGroupModelFilter.mergeTabsToGroup(tabC.getId(), tabA.getId());
                    mTabGroupModelFilter.setTabGroupTitle(tabA.getId(), "Bar");
                    titles[1] = tabA.getTitle();
                    titles[0] = tabC.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA, tabC))
                                    .hideTabGroups(true)
                                    .build());
                    mTabModel.commitTabClosure(tabA.getId());
                    mTabModel.commitTabClosure(tabC.getId());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(2, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0), RecentlyClosedBulkEvent.class, new String[] {}, titles, urls);

        final RecentlyClosedBulkEvent bulkEvent = (RecentlyClosedBulkEvent) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedEntry(mTabModel, bulkEvent);
                });

        // 1. Blank tab
        // 2. tabB
        // 3. tabA restored in new tab.
        // 4. tabC restored in new tab.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(4, tabs.size());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(2)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(2)).getSpec());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(3)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(3)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(2)));
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(3)));
                });
    }

    /** Tests opening a tab that is a subset of a group. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedEntry_SingleTab_FromGroupSubsetClosure_Undoable() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls = new String[] {getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);
        final Tab tabB =
                sActivityTestRule.loadUrlInNewTab(getUrl(TEST_PAGE_B), /* incognito= */ false);

        final String[] titles = new String[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    mTabGroupModelFilter.setTabGroupTitle(tabA.getId(), "Bar");
                    titles[0] = tabA.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA)).hideTabGroups(true).build());
                    mTabModel.commitTabClosure(tabA.getId());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(2, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(recentEntries.get(0), RecentlyClosedTab.class, new String[] {}, titles, urls);

        final RecentlyClosedTab recentTab = (RecentlyClosedTab) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedEntry(mTabModel, recentTab);
                });

        // 1. Blank tab
        // 2. tabB
        // 3. tabA restored in new tab.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(3, tabs.size());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(2)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(2)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(2)));
                });
    }

    /** Tests opening a specific closed single tab group that is not undoable. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedEntry_SingleTabGroupSupported_FromGroupClosure_NotUndoable() {
        if (mTabGroupModelFilter == null) return;

        final String[] urls = new String[] {getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.createSingleTabGroup(tabA, /* notify= */ false);
                    mTabGroupModelFilter.setTabGroupTitle(tabA.getId(), "Bar");
                    titles[0] = tabA.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA))
                                    .allowUndo(false)
                                    .hideTabGroups(true)
                                    .build());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0),
                RecentlyClosedGroup.class,
                new String[] {"Bar"},
                titles,
                urls);

        final RecentlyClosedGroup group = (RecentlyClosedGroup) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedEntry(mTabModel, group);
                });

        // 1. Blank tab
        // 2. tabA restored in new tab group.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(2, tabs.size());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertEquals(
                            "Bar", mTabGroupModelFilter.getTabGroupTitle(tabs.get(1).getId()));
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                });
    }

    /** Tests opening a specific closed single tab group. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedEntry_SingleTabGroupSupported_FromGroupClosure_Undoable() {
        if (mTabGroupModelFilter == null) return;

        final String[] urls = new String[] {getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.createSingleTabGroup(tabA, /* notify= */ false);
                    mTabGroupModelFilter.setTabGroupTitle(tabA.getId(), "Bar");
                    titles[0] = tabA.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA)).hideTabGroups(true).build());
                    mTabModel.commitTabClosure(tabA.getId());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0),
                RecentlyClosedGroup.class,
                new String[] {"Bar"},
                titles,
                urls);

        final RecentlyClosedGroup group = (RecentlyClosedGroup) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedEntry(mTabModel, group);
                });

        // 1. Blank tab
        // 2. tabA restored in new tab.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(2, tabs.size());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertEquals(
                            "Bar", mTabGroupModelFilter.getTabGroupTitle(tabs.get(1).getId()));
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                });
    }

    /** Tests a hiding tab group is not saved when undoable. */
    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_SYNC_ANDROID})
    public void testNoRecentlyClosedEntry_ForHidingTabGroup_Undoable() {
        if (mTabGroupModelFilter == null) return;

        final String[] urls = new String[] {getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.createSingleTabGroup(tabA, /* notify= */ false);
                    mTabGroupModelFilter.setTabGroupTitle(tabA.getId(), "Bar");
                    titles[0] = tabA.getTitle();
                    mTabGroupModelFilter.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA)).hideTabGroups(true).build());
                    mTabModel.commitTabClosure(tabA.getId());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(0, recentEntries.size());
    }

    /** Tests a hiding tab group is not saved when not undoable. */
    @Test
    @MediumTest
    @EnableFeatures({ChromeFeatureList.TAB_GROUP_SYNC_ANDROID})
    public void testNoRecentlyClosedEntry_ForHidingTabGroup_NotUndoable() {
        if (mTabGroupModelFilter == null) return;

        final String[] urls = new String[] {getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.createSingleTabGroup(tabA, /* notify= */ false);
                    mTabGroupModelFilter.setTabGroupTitle(tabA.getId(), "Bar");
                    titles[0] = tabA.getTitle();
                    mTabGroupModelFilter.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA))
                                    .allowUndo(false)
                                    .hideTabGroups(true)
                                    .build());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(0, recentEntries.size());
    }

    /** Tests opening a specific closed group and that it persists across restarts. */
    @Test
    @LargeTest
    public void testOpenRecentlyClosedEntry_Group_FromGroupClosure_WithRestart() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls =
                new String[] {getUrl(TEST_PAGE_C), getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[2], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabC = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[3];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    mTabGroupModelFilter.mergeTabsToGroup(tabC.getId(), tabA.getId());
                    mTabGroupModelFilter.setTabGroupTitle(tabA.getId(), "Bar");
                    titles[2] = tabA.getTitle();
                    titles[1] = tabB.getTitle();
                    titles[0] = tabC.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA, tabB, tabC))
                                    .allowUndo(false)
                                    .hideTabGroups(true)
                                    .build());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0),
                RecentlyClosedGroup.class,
                new String[] {"Bar"},
                titles,
                urls);

        final RecentlyClosedGroup group = (RecentlyClosedGroup) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedEntry(mTabModel, group);
                });

        // 1. Blank tab
        // 2. tabA restored in new tab.
        // 3. tabB restored in new tab.
        // 4. tabC restored in new tab.
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(4, tabs.size());
        Assert.assertEquals(titles[2], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[2], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(2)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(2)).getSpec());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(3)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(3)).getSpec());
        final int[] tabIds =
                new int[] {tabs.get(1).getId(), tabs.get(2).getId(), tabs.get(3).getId()};
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertEquals(
                            "Bar", mTabGroupModelFilter.getTabGroupTitle(tabs.get(1).getId()));
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(2)));
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(3)));
                    Assert.assertEquals(
                            Arrays.asList(new Tab[] {tabs.get(1), tabs.get(2), tabs.get(3)}),
                            mTabGroupModelFilter.getRelatedTabList(tabs.get(1).getId()));
                });

        // Restart activity.
        restartActivity();

        // Confirm the same tabs are present with the same group structure.
        tabs.clear();
        tabs.addAll(getAllTabs());
        Assert.assertEquals(4, tabs.size());
        Assert.assertEquals(tabIds[0], tabs.get(1).getId());
        Assert.assertEquals(titles[2], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[2], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        Assert.assertEquals(tabIds[1], tabs.get(2).getId());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(2)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(2)).getSpec());
        Assert.assertEquals(tabIds[2], tabs.get(3).getId());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(3)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(3)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertEquals(
                            "Bar", mTabGroupModelFilter.getTabGroupTitle(tabs.get(1).getId()));
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(2)));
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(3)));
                    Assert.assertEquals(
                            Arrays.asList(new Tab[] {tabs.get(1), tabs.get(2), tabs.get(3)}),
                            mTabGroupModelFilter.getRelatedTabList(tabs.get(1).getId()));
                });
    }

    /** Tests opening a specific closed {@link Tab} that was closed as part of a bulk closure. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedEntry_Tab_FromBulkClosure() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls =
                new String[] {getUrl(TEST_PAGE_C), getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[2], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabC = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[3];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    titles[2] = tabA.getTitle();
                    titles[1] = tabB.getTitle();
                    titles[0] = tabC.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA, tabB, tabC))
                                    .hideTabGroups(true)
                                    .build());
                    mTabModel.commitAllTabClosures();
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0),
                RecentlyClosedBulkEvent.class,
                new String[] {""},
                titles,
                urls);

        final RecentlyClosedBulkEvent event = (RecentlyClosedBulkEvent) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    // Tab order is inverted as most recent comes first so pick the last tab to
                    // restore ==
                    // tabC.
                    mRecentlyClosedBridge.openRecentlyClosedEntry(
                            mTabModel, event.getTabs().get(0));
                });

        // 1. Blank tab
        // 2. Restored tabC
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(2, tabs.size());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
    }

    /** Tests opening a specific closed bulk closure. */
    @Test
    @MediumTest
    public void testOpenRecentlyClosedEntry_Bulk_FromBulkClosure() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls =
                new String[] {getUrl(TEST_PAGE_C), getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[2], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabC = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[3];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    mTabGroupModelFilter.setTabGroupTitle(tabA.getId(), "Foo");
                    titles[2] = tabA.getTitle();
                    titles[1] = tabB.getTitle();
                    titles[0] = tabC.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA, tabB, tabC))
                                    .hideTabGroups(true)
                                    .build());
                    mTabModel.commitAllTabClosures();
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0),
                RecentlyClosedBulkEvent.class,
                new String[] {"Foo"},
                titles,
                urls);

        final RecentlyClosedBulkEvent event = (RecentlyClosedBulkEvent) recentEntries.get(0);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openRecentlyClosedEntry(mTabModel, event);
                });

        // 1. Blank tab
        // 2. Restored tabA
        // 3. Restored tabB
        // 4. Restored tabC
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(4, tabs.size());
        Assert.assertEquals(titles[2], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[2], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(2)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(2)).getSpec());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(3)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(3)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertEquals(
                            "Foo", mTabGroupModelFilter.getTabGroupTitle(tabs.get(1).getId()));
                    Assert.assertEquals(
                            Arrays.asList(new Tab[] {tabs.get(1), tabs.get(2)}),
                            mTabGroupModelFilter.getRelatedTabList(tabs.get(1).getId()));
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(3)));
                });
    }

    /** Tests opening the most recent group closure. */
    @Test
    @MediumTest
    public void testOpenMostRecentlyClosedEntry_Group() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] groupUrls = new String[] {getUrl(TEST_PAGE_C), getUrl(TEST_PAGE_B)};
        final String[] url = new String[] {getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(url[0], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(groupUrls[1], /* incognito= */ false);
        final Tab tabC = sActivityTestRule.loadUrlInNewTab(groupUrls[0], /* incognito= */ false);

        final String[] groupTitles = new String[2];
        final String[] title = new String[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabC.getId(), tabB.getId());
                    title[0] = tabA.getTitle();
                    groupTitles[1] = tabB.getTitle();
                    groupTitles[0] = tabC.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabB, tabC))
                                    .hideTabGroups(true)
                                    .build());
                    mTabModel.closeTabs(TabClosureParams.closeTab(tabA).build());
                    mTabModel.commitTabClosure(tabB.getId());
                    mTabModel.commitTabClosure(tabA.getId());
                    mTabModel.commitTabClosure(tabC.getId());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(2, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0),
                RecentlyClosedGroup.class,
                new String[] {""},
                groupTitles,
                groupUrls);
        assertEntryIs(recentEntries.get(1), RecentlyClosedTab.class, new String[0], title, url);

        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openMostRecentlyClosedEntry(mTabModel);
                });

        // 1. Blank tab
        // 2. Restored tabB
        // 3. Restored tabC
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(3, tabs.size());
        Assert.assertEquals(groupTitles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(groupUrls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        Assert.assertEquals(groupTitles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(2)));
        Assert.assertEquals(groupUrls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(2)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertNull(mTabGroupModelFilter.getTabGroupTitle(tabs.get(1).getId()));
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                    Assert.assertEquals(
                            Arrays.asList(new Tab[] {tabs.get(1), tabs.get(2)}),
                            mTabGroupModelFilter.getRelatedTabList(tabs.get(1).getId()));
                });

        tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(3, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(recentEntries.get(0), RecentlyClosedTab.class, new String[0], title, url);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openMostRecentlyClosedEntry(mTabModel);
                });

        // 1. Blank tab
        // 2. Restored tabB
        // 3. Restored tabC
        // 4. Restored tabA
        tabs.clear();
        tabs.addAll(getAllTabs());
        Assert.assertEquals(4, tabs.size());
        Assert.assertEquals(title[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(3)));
        Assert.assertEquals(url[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(3)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(3)));
                });
    }

    /** Tests opening the most recent bulk closure. */
    @Test
    @MediumTest
    public void testOpenMostRecentlyClosedEntry_Bulk() {
        if (mTabGroupModelFilter == null) return;

        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls =
                new String[] {getUrl(TEST_PAGE_C), getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[2], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabC = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[3];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    titles[2] = tabA.getTitle();
                    titles[1] = tabB.getTitle();
                    titles[0] = tabC.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA, tabB, tabC))
                                    .hideTabGroups(true)
                                    .build());
                    mTabModel.commitAllTabClosures();
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(1, recentEntries.size());
        assertEntryIs(
                recentEntries.get(0),
                RecentlyClosedBulkEvent.class,
                new String[] {""},
                titles,
                urls);
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mRecentlyClosedBridge.openMostRecentlyClosedEntry(mTabModel);
                });

        // 1. Blank tab
        // 2. Restored tabA
        // 3. Restored tabB
        // 4. Restored tabC
        final List<Tab> tabs = getAllTabs();
        Assert.assertEquals(4, tabs.size());
        Assert.assertEquals(titles[2], ChromeTabUtils.getTitleOnUiThread(tabs.get(1)));
        Assert.assertEquals(urls[2], ChromeTabUtils.getUrlOnUiThread(tabs.get(1)).getSpec());
        Assert.assertEquals(titles[1], ChromeTabUtils.getTitleOnUiThread(tabs.get(2)));
        Assert.assertEquals(urls[1], ChromeTabUtils.getUrlOnUiThread(tabs.get(2)).getSpec());
        Assert.assertEquals(titles[0], ChromeTabUtils.getTitleOnUiThread(tabs.get(3)));
        Assert.assertEquals(urls[0], ChromeTabUtils.getUrlOnUiThread(tabs.get(3)).getSpec());
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    Assert.assertTrue(mTabGroupModelFilter.isTabInTabGroup(tabs.get(1)));
                    Assert.assertNull(mTabGroupModelFilter.getTabGroupTitle(tabs.get(1).getId()));
                    Assert.assertEquals(
                            Arrays.asList(new Tab[] {tabs.get(1), tabs.get(2)}),
                            mTabGroupModelFilter.getRelatedTabList(tabs.get(1).getId()));
                    Assert.assertFalse(mTabGroupModelFilter.isTabInTabGroup(tabs.get(3)));
                });
    }

    /** Tests tabs are not saved when unrestorable. */
    @Test
    @MediumTest
    public void testNoRecentlyClosedEntry_FromBulkClosure_Unrestorable() {
        final String[] urls = new String[] {getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    titles[1] = tabA.getTitle();
                    titles[0] = tabB.getTitle();
                    mTabModel.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabB, tabA))
                                    .allowUndo(false)
                                    .hideTabGroups(true)
                                    .saveToTabRestoreService(false)
                                    .build());
                    mTabModel.commitAllTabClosures();
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(0, recentEntries.size());
    }

    /** Tests tab groups are not saved when unrestorable. */
    @Test
    @MediumTest
    public void testNoRecentlyClosedEntry_FromGroupClosure_Unrestorable() {
        if (mTabGroupModelFilter == null) return;

        final String[] urls = new String[] {getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    mTabGroupModelFilter.mergeTabsToGroup(tabB.getId(), tabA.getId());
                    titles[1] = tabA.getTitle();
                    titles[0] = tabB.getTitle();
                    mTabGroupModelFilter.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabB, tabA))
                                    .allowUndo(false)
                                    .hideTabGroups(true)
                                    .saveToTabRestoreService(false)
                                    .build());
                });

        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCount = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(1, tabCount);
        Assert.assertEquals(0, recentEntries.size());
    }

    /** Tests closing a tab will be saved as a TAB session entry in tab restore service. */
    @Test
    @MediumTest
    public void testCloseTabSaveAsTabSessionRestoreEntry() {
        final String[] urls = new String[] {getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    titles[0] = tabA.getTitle();
                    mTabModel.closeTabs(TabClosureParams.closeTab(tabA).build());
                    mTabModel.commitTabClosure(tabA.getId());
                });
        final List<RecentlyClosedEntry> recentEntries = new ArrayList();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    recentEntries.addAll(
                            mRecentlyClosedBridge.getRecentlyClosedEntries(MAX_ENTRY_COUNT));
                });
        Assert.assertEquals(1, recentEntries.size());
        RecentlyClosedEntry recentEntry = recentEntries.get(0);
        // Verify recentEntry is from a TAB session entry returned by tab restore service.
        // Note: RecentlyClosedBridgeJni.getRecentlyClosedEntries returns a RecentlyClosedTab
        // instance for a session entry of type sessions::tab_restore::Type::TAB.
        Assert.assertTrue(RecentlyClosedTab.class.isInstance(recentEntry));
        final List<RecentlyClosedTab> recentTabs =
                (List<RecentlyClosedTab>) (List<? extends RecentlyClosedEntry>) recentEntries;
        Assert.assertEquals(1, recentTabs.size());
        assertTabsAre(recentTabs, titles, urls);
    }

    /** Tests closing all tabs will be saved as a WINDOW session entry in tab restore service. */
    @Test
    @MediumTest
    public void testCloseAllTabsSaveAsWindowSessionRestoreEntry() {
        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls = new String[] {getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[2];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    titles[1] = tabA.getTitle();
                    titles[0] = tabB.getTitle();
                    mTabModel.closeTabs(TabClosureParams.closeAllTabs().build());
                    mTabModel.commitAllTabClosures();
                });
        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    recentEntries.addAll(
                            mRecentlyClosedBridge.getRecentlyClosedEntries(MAX_ENTRY_COUNT));
                });
        Assert.assertEquals(1, recentEntries.size());
        RecentlyClosedEntry recentEntry = recentEntries.get(0);
        // Verify recentEntry is from a WINDOW session entry returned by tab restore service.
        // Note: RecentlyClosedBridgeJni.getRecentlyClosedEntries returns a RecentlyClosedBulkEvent
        // instance for a session entry of type sessions::tab_restore::Type::WINDOW.
        Assert.assertTrue(RecentlyClosedBulkEvent.class.isInstance(recentEntry));
        final RecentlyClosedBulkEvent event = (RecentlyClosedBulkEvent) recentEntry;
        final List<RecentlyClosedTab> recentTabs = event.getTabs();
        Assert.assertEquals(2, recentTabs.size());
        assertTabsAre(recentTabs, titles, urls);
    }

    /**
     * Tests closing multiple tabs will be saved as a WINDOW session entry in tab restore service.
     */
    @Test
    @MediumTest
    public void testCloseMultipleTabsSaveAsWindowSessionRestoreEntry() {
        // Tab order is inverted in RecentlyClosedEntry as most recent comes first so log data in
        // reverse.
        final String[] urls = new String[] {getUrl(TEST_PAGE_B), getUrl(TEST_PAGE_A)};
        final Tab tabA = sActivityTestRule.loadUrlInNewTab(urls[1], /* incognito= */ false);
        final Tab tabB = sActivityTestRule.loadUrlInNewTab(urls[0], /* incognito= */ false);

        final String[] titles = new String[2];
        final int[] tabCountBeforeClosingTabs = new int[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    titles[1] = tabA.getTitle();
                    titles[0] = tabB.getTitle();
                    tabCountBeforeClosingTabs[0] = mTabModel.getCount();
                    mTabGroupModelFilter.closeTabs(
                            TabClosureParams.closeTabs(List.of(tabA, tabB))
                                    .hideTabGroups(true)
                                    .build());
                    mTabModel.commitAllTabClosures();
                });
        final List<RecentlyClosedEntry> recentEntries = new ArrayList<>();
        final int tabCountAfterClosingTabs = getRecentEntriesAndReturnActiveTabCount(recentEntries);
        Assert.assertEquals(3, tabCountBeforeClosingTabs[0]);
        Assert.assertEquals(1, tabCountAfterClosingTabs);
        Assert.assertEquals(1, recentEntries.size());
        RecentlyClosedEntry recentEntry = recentEntries.get(0);
        // Verify recentEntry is from a WINDOW session entry returned by tab restore service.
        // Note: RecentlyClosedBridgeJni.getRecentlyClosedEntries returns a RecentlyClosedBulkEvent
        // instance for a session entry of type sessions::tab_restore::Type::WINDOW.
        Assert.assertTrue(RecentlyClosedBulkEvent.class.isInstance(recentEntry));
        final RecentlyClosedBulkEvent event = (RecentlyClosedBulkEvent) recentEntry;
        final List<RecentlyClosedTab> recentTabs = event.getTabs();
        Assert.assertEquals(2, recentTabs.size());
        assertTabsAre(recentTabs, titles, urls);
    }

    // TODO(crbug.com/40218713): Add a test a case where bulk closures remain in the native service,
    // but the flag state is flipped.

    private Tab findTabWithUrlAndTitle(TabList list, String url, String title) {
        Tab targetTab = null;
        for (int i = 0; i < list.getCount(); ++i) {
            Tab tab = list.getTabAt(i);
            if (tab.getUrl().getSpec().equals(url) && tab.getTitle().equals(title)) {
                targetTab = tab;
                break;
            }
        }
        return targetTab;
    }

    private String getUrl(String relativeUrl) {
        return sActivityTestRule.getTestServer().getURL(relativeUrl);
    }

    private Tab freezeTab(Tab tab) {
        Tab[] frozen = new Tab[1];
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    TabState state = TabStateExtractor.from(tab);
                    mActivity
                            .getCurrentTabModel()
                            .closeTabs(TabClosureParams.closeTab(tab).allowUndo(false).build());
                    frozen[0] =
                            mActivity.getCurrentTabCreator().createFrozenTab(state, tab.getId(), 1);
                });
        return frozen[0];
    }

    private void assertTabsAre(List<RecentlyClosedTab> tabs, String[] titles, String[] urls) {
        assert titles.length == urls.length;
        Assert.assertEquals("Unexpected number of tabs.", titles.length, tabs.size());
        for (int i = 0; i < titles.length; i++) {
            Assert.assertEquals("Tab " + i + " title mismatch.", titles[i], tabs.get(i).getTitle());
            Assert.assertEquals(
                    "Tab " + i + " url mismatch.", urls[i], tabs.get(i).getUrl().getSpec());
        }
    }

    private void assertEntryIs(
            RecentlyClosedEntry entry,
            Class<? extends RecentlyClosedEntry> cls,
            String[] groupTitles,
            String[] titles,
            String[] urls) {
        assert titles.length == urls.length;
        Assert.assertTrue(cls.isInstance(entry));

        if (cls == RecentlyClosedTab.class) {
            assert groupTitles.length == 0;
            assert titles.length == 1;
            RecentlyClosedTab tab = (RecentlyClosedTab) entry;
            assertTabsAre(Collections.singletonList(tab), titles, urls);
            return;
        }
        if (cls == RecentlyClosedGroup.class) {
            assert groupTitles.length == 1;
            RecentlyClosedGroup group = (RecentlyClosedGroup) entry;
            Assert.assertEquals(groupTitles[0], group.getTitle());
            assertTabsAre(group.getTabs(), titles, urls);
            return;
        }

        RecentlyClosedBulkEvent event = (RecentlyClosedBulkEvent) entry;
        final List<String> expectedTitles = Arrays.asList(groupTitles);
        final List<String> actualTitles = new ArrayList<>(event.getTabGroupIdToTitleMap().values());
        Assert.assertEquals(expectedTitles.size(), actualTitles.size());
        Assert.assertTrue(
                expectedTitles.containsAll(actualTitles)
                        && actualTitles.containsAll(expectedTitles));
        assertTabsAre(event.getTabs(), titles, urls);
    }

    private List<Tab> getAllTabs() {
        final List<Tab> list = new ArrayList<>();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    for (int i = 0; i < mTabModel.getCount(); i++) {
                        list.add(mTabModel.getTabAt(i));
                    }
                });
        return list;
    }

    private int getRecentEntriesAndReturnActiveTabCount(final List<RecentlyClosedEntry> entries) {
        final int[] tabCount = new int[1];
        entries.clear();
        ThreadUtils.runOnUiThreadBlocking(
                () -> {
                    entries.addAll(mRecentlyClosedBridge.getRecentlyClosedEntries(MAX_ENTRY_COUNT));
                    tabCount[0] = mTabModel.getCount();
                });
        return tabCount[0];
    }

    private void restartActivity() {
        ThreadUtils.runOnUiThreadBlocking(mActivity::saveState);
        sActivityTestRule.recreateActivity();
        mActivity = sActivityTestRule.getActivity();
        mTabModelSelector = mActivity.getTabModelSelectorSupplier().get();
        CriteriaHelper.pollUiThread(mTabModelSelector::isTabStateInitialized);
        mTabModel = mTabModelSelector.getModel(false);
        TabModelFilter filter =
                mTabModelSelector.getTabModelFilterProvider().getTabModelFilter(false);
        assert filter instanceof TabGroupModelFilter;
        mTabGroupModelFilter = (TabGroupModelFilter) filter;
    }
}