chromium/ios/chrome/browser/ui/tab_switcher/tab_grid/grid/base_grid_mediator_unittest.mm

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

#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/base_grid_mediator.h"

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

#import <memory>

#import "base/apple/foundation_util.h"
#import "base/containers/contains.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/time/time.h"
#import "components/commerce/core/commerce_feature_list.h"
#import "components/saved_tab_groups/mock_tab_group_sync_service.h"
#import "components/saved_tab_groups/saved_tab_group.h"
#import "components/tab_groups/tab_group_id.h"
#import "components/tab_groups/tab_group_visual_data.h"
#import "ios/chrome/browser/commerce/model/shopping_persisted_data_tab_helper.h"
#import "ios/chrome/browser/drag_and_drop/model/drag_item_util.h"
#import "ios/chrome/browser/main/model/browser_web_state_list_delegate.h"
#import "ios/chrome/browser/saved_tab_groups/model/tab_group_sync_service_factory.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group.h"
#import "ios/chrome/browser/shared/model/web_state_list/test/web_state_list_builder_from_description.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/snapshots/model/snapshot_browser_agent.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_drag_drop_metrics.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_item_identifier.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_mediator_test.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/incognito/incognito_grid_mediator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/regular/regular_grid_mediator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_grid_mode_holder.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_toolbars_configuration.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/test/fake_tab_grid_toolbars_mediator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_group_item.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_item.h"
#import "ios/chrome/browser/ui/tab_switcher/test/fake_tab_collection_consumer.h"
#import "ios/chrome/browser/ui/tab_switcher/web_state_tab_switcher_item.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_id.h"
#import "testing/gtest_mac.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"

using tab_groups::TabGroupId;

namespace {

// URL to be used for drag and drop tests.
const char kDraggedUrl[] = "https://dragged_url.com";

// BaseGridMediatorTest is parameterized on this enum to test all children
// mediator.
enum GridMediatorType { TEST_REGULAR_MEDIATOR, TEST_INCOGNITO_MEDIATOR };

const char kHasPriceDropUserAction[] = "Commerce.TabGridSwitched.HasPriceDrop";
const char kHasNoPriceDropUserAction[] = "Commerce.TabGridSwitched.NoPriceDrop";

// Returns a test `SavedTabGroup`.
tab_groups::SavedTabGroup TestSavedGroup() {
  tab_groups::SavedTabGroup saved_group(
      u"Test title", tab_groups::TabGroupColorId::kBlue, {}, std::nullopt,
      base::Uuid::GenerateRandomV4(), tab_groups::TabGroupId::GenerateNew());
  return saved_group;
}

}  // namespace

class BaseGridMediatorTest
    : public GridMediatorTestClass,
      public ::testing::WithParamInterface<GridMediatorType> {
 public:
  BaseGridMediatorTest() {}
  ~BaseGridMediatorTest() override {}

  void SetUp() override {
    GridMediatorTestClass::SetUp();
    mode_holder_ = [[TabGridModeHolder alloc] init];
    if (GetParam() == TEST_INCOGNITO_MEDIATOR) {
      mediator_ =
          [[IncognitoGridMediator alloc] initWithModeHolder:mode_holder_];
    } else {
      mediator_ = [[RegularGridMediator alloc] initWithModeHolder:mode_holder_];
    }
    mediator_.consumer = consumer_;
    mediator_.browser = browser_.get();
    mediator_.toolbarsMutator = fake_toolbars_mediator_;
    [mediator_ currentlySelectedGrid:YES];
  }

  void TearDown() override {
    // Forces the IncognitoGridMediator to removes its Observer from
    // WebStateList before the Browser is destroyed.
    mediator_.browser = nullptr;
    mediator_ = nil;
    GridMediatorTestClass::TearDown();
  }

  // Checks that the drag item origin metric is logged in UMA.
  void ExpectThatDragItemOriginMetricLogged(DragItemOrigin origin,
                                            int count = 1) {
    histogram_tester_.ExpectUniqueSample(kUmaGridViewDragOrigin, origin, count);
  }

 protected:
  BaseGridMediator* mediator_;
  base::HistogramTester histogram_tester_;
  TabGridModeHolder* mode_holder_;
};

// Variation on BaseGridMediatorTest which enable PriceDropIndicatorsFlag.
class BaseGridMediatorWithPriceDropIndicatorsTest
    : public BaseGridMediatorTest {
 protected:

  void SetFakePriceDrop(web::WebState* web_state) {
    ShoppingPersistedDataTabHelper::PriceDrop price_drop;
    price_drop.current_price = @"$5";
    price_drop.previous_price = @"$10";
    price_drop.url = web_state->GetLastCommittedURL();
    price_drop.timestamp = base::Time::Now();
    ShoppingPersistedDataTabHelper::FromWebState(web_state)
        ->SetPriceDropForTesting(
            std::make_unique<ShoppingPersistedDataTabHelper::PriceDrop>(
                std::move(price_drop)));
  }
};

#pragma mark - Consumer tests

// Tests drag and dropping an item that has been closed.
TEST_P(BaseGridMediatorTest, DragAndDropClosedItem) {
  std::unique_ptr<web::FakeWebState> web_state =
      CreateFakeWebStateWithURL(GURL("https://google.com"));
  web::WebState* web_state_ptr = web_state.get();
  browser_->GetWebStateList()->InsertWebState(
      std::move(web_state), WebStateList::InsertionParams::AtIndex(1));

  mode_holder_.mode = TabGridMode::kSelection;
  [mediator_
      addToSelectionItemID:[GridItemIdentifier tabIdentifier:web_state_ptr]];

  browser_->GetWebStateList()->CloseWebStateAt(1,
                                               WebStateList::CLOSE_USER_ACTION);
  EXPECT_EQ(0UL, [mediator_ allSelectedDragItems].count);
}

// Tests that the consumer is populated after the tab model is set on the
// mediator.
TEST_P(BaseGridMediatorTest, ConsumerPopulateItems) {
  EXPECT_EQ(3UL, consumer_.items.size());
  EXPECT_EQ(original_selected_identifier_,
            consumer_.selectedItem.tabSwitcherItem.identifier);
}

// Tests that the consumer is notified when a web state is inserted.
TEST_P(BaseGridMediatorTest, ConsumerInsertItem) {
  ASSERT_EQ(3UL, consumer_.items.size());
  std::unique_ptr<web::FakeWebState> web_state =
      CreateFakeWebStateWithURL(GURL());
  web::WebStateID item_identifier = web_state.get()->GetUniqueIdentifier();
  browser_->GetWebStateList()->InsertWebState(
      std::move(web_state), WebStateList::InsertionParams::AtIndex(1));
  EXPECT_EQ(4UL, consumer_.items.size());
  // The same ID should be selected after the insertion, since the new web state
  // wasn't selected.
  EXPECT_EQ(original_selected_identifier_,
            consumer_.selectedItem.tabSwitcherItem.identifier);
  EXPECT_EQ(item_identifier, consumer_.items[1]);
  EXPECT_FALSE(base::Contains(original_identifiers_, item_identifier));
}

// Tests that the consumer is notified when a web state is removed.
// The selected web state at index 1 is removed. The web state originally
// at index 2 should be the new selected item.
TEST_P(BaseGridMediatorTest, ConsumerRemoveItem) {
  browser_->GetWebStateList()->CloseWebStateAt(1, WebStateList::CLOSE_NO_FLAGS);
  EXPECT_EQ(2UL, consumer_.items.size());
  // Expect that a different web state is selected now.
  EXPECT_NE(original_selected_identifier_,
            consumer_.selectedItem.tabSwitcherItem.identifier);
}

// Tests that the consumer is notified when the active web state is changed.
TEST_P(BaseGridMediatorTest, ConsumerUpdateSelectedItem) {
  EXPECT_EQ(original_selected_identifier_,
            consumer_.selectedItem.tabSwitcherItem.identifier);
  browser_->GetWebStateList()->ActivateWebStateAt(2);
  EXPECT_EQ(
      browser_->GetWebStateList()->GetWebStateAt(2)->GetUniqueIdentifier(),
      consumer_.selectedItem.tabSwitcherItem.identifier);
}

// Tests that the consumer is notified when a web state is replaced.
// The selected item is replaced, so the new selected item id should be the
// id of the new item.
TEST_P(BaseGridMediatorTest, ConsumerReplaceItem) {
  std::unique_ptr<web::FakeWebState> new_web_state =
      CreateFakeWebStateWithURL(GURL());
  web::WebStateID new_item_identifier = new_web_state->GetUniqueIdentifier();
  @autoreleasepool {
    browser_->GetWebStateList()->ReplaceWebStateAt(1, std::move(new_web_state));
  }
  EXPECT_EQ(3UL, consumer_.items.size());
  EXPECT_EQ(new_item_identifier,
            consumer_.selectedItem.tabSwitcherItem.identifier);
  EXPECT_EQ(new_item_identifier, consumer_.items[1]);
  EXPECT_FALSE(base::Contains(original_identifiers_, new_item_identifier));
}

// Tests that the consumer is notified when a web state is moved.
TEST_P(BaseGridMediatorTest, ConsumerMoveItem) {
  web::WebStateID item1 = consumer_.items[1];
  web::WebStateID item2 = consumer_.items[2];
  browser_->GetWebStateList()->MoveWebStateAt(1, 2);
  EXPECT_EQ(item1, consumer_.items[2]);
  EXPECT_EQ(item2, consumer_.items[1]);
}

#pragma mark - Command tests

// Tests that the active index is updated when `-selectItemWithID:` is called.
// Tests that the consumer's selected index is updated.
TEST_P(BaseGridMediatorTest, SelectItemCommand) {
  // Previous selected index is 1.
  web::WebStateID identifier =
      browser_->GetWebStateList()->GetWebStateAt(2)->GetUniqueIdentifier();
  [mediator_ selectItemWithID:identifier pinned:NO isFirstActionOnTabGrid:NO];
  EXPECT_EQ(2, browser_->GetWebStateList()->active_index());
  EXPECT_EQ(identifier, consumer_.selectedItem.tabSwitcherItem.identifier);
}

// Tests that the active index is updated when `-selectItemWithID:` is called.
// Tests that the consumer's selected index is updated with pinned state.
TEST_P(BaseGridMediatorTest, SelectPinnedItemCommand) {
  if (GetParam() == TEST_INCOGNITO_MEDIATOR || !IsPinnedTabsEnabled()) {
    // Test only available in non-incognito when pinned tabs are enabled.
    return;
  }
  WebStateList* web_state_list = browser_->GetWebStateList();
  web::WebStateID identifier_0 =
      web_state_list->GetWebStateAt(0)->GetUniqueIdentifier();
  web::WebStateID identifier_1 =
      web_state_list->GetWebStateAt(1)->GetUniqueIdentifier();
  web::WebStateID identifier_2 =
      web_state_list->GetWebStateAt(2)->GetUniqueIdentifier();
  [mediator_ setPinState:YES forItemWithID:identifier_0];
  ASSERT_EQ(1, browser_->GetWebStateList()->active_index());
  ASSERT_EQ(identifier_1, consumer_.selectedItem.tabSwitcherItem.identifier);

  [mediator_ selectItemWithID:identifier_0
                       pinned:YES
       isFirstActionOnTabGrid:NO];

  EXPECT_EQ(0, browser_->GetWebStateList()->active_index());
  EXPECT_EQ(identifier_0, consumer_.selectedItem.tabSwitcherItem.identifier);

  [mediator_ selectItemWithID:identifier_2 pinned:NO isFirstActionOnTabGrid:NO];

  EXPECT_EQ(2, browser_->GetWebStateList()->active_index());
  EXPECT_EQ(identifier_2, consumer_.selectedItem.tabSwitcherItem.identifier);

  // Selecting the pinned one with pinned = NO fails.
  [mediator_ selectItemWithID:identifier_0 pinned:NO isFirstActionOnTabGrid:NO];

  EXPECT_EQ(2, browser_->GetWebStateList()->active_index());
  EXPECT_EQ(identifier_2, consumer_.selectedItem.tabSwitcherItem.identifier);
}

// Tests the pinned tab command.
TEST_P(BaseGridMediatorTest, PinItemCommand) {
  if (GetParam() == TEST_INCOGNITO_MEDIATOR || !IsPinnedTabsEnabled()) {
    // Test only available in non-incognito when pinned tabs are enabled.
    return;
  }
  WebStateList* web_state_list = browser_->GetWebStateList();
  // At first the second web state is active.
  ASSERT_EQ(1, web_state_list->active_index());
  ASSERT_EQ(0, web_state_list->pinned_tabs_count());

  web::WebStateID selected_identifier =
      web_state_list->GetWebStateAt(1)->GetUniqueIdentifier();
  web::WebStateID identifier =
      web_state_list->GetWebStateAt(2)->GetUniqueIdentifier();

  [mediator_ setPinState:YES forItemWithID:identifier];

  // The pinned web state moved to the first position, moving the others.
  EXPECT_EQ(1, web_state_list->pinned_tabs_count());
  EXPECT_EQ(2, web_state_list->active_index());
  EXPECT_EQ(selected_identifier,
            consumer_.selectedItem.tabSwitcherItem.identifier);

  [mediator_ setPinState:NO forItemWithID:identifier];

  // The pinned web state moves back to the end of the WebStateList.
  EXPECT_EQ(0, web_state_list->pinned_tabs_count());
  EXPECT_EQ(1, web_state_list->active_index());
  EXPECT_EQ(selected_identifier,
            consumer_.selectedItem.tabSwitcherItem.identifier);
}

// Tests that the WebStateList count is decremented when
// `-closeItemWithID:` is called.
// Tests that the consumer's item count is also decremented.
TEST_P(BaseGridMediatorTest, CloseItemCommand) {
  // Previously there were 3 items.
  web::WebStateID identifier =
      browser_->GetWebStateList()->GetWebStateAt(0)->GetUniqueIdentifier();
  [mediator_ closeItemWithID:identifier];
  EXPECT_EQ(2, browser_->GetWebStateList()->count());
  EXPECT_EQ(2UL, consumer_.items.size());
}

// Tests that when `-addNewItem` is called, the WebStateList count is
// incremented, the `active_index` is at the end of WebStateList, the new
// web state has no opener, and the URL is the New Tab Page.
// Tests that the consumer has added an item with the correct identifier.
TEST_P(BaseGridMediatorTest, AddNewItemAtEndCommand) {
  // Previously there were 3 items and the selected index was 1.
  [mediator_ addNewItem];
  EXPECT_EQ(4, browser_->GetWebStateList()->count());
  EXPECT_EQ(3, browser_->GetWebStateList()->active_index());
  web::WebState* web_state = browser_->GetWebStateList()->GetWebStateAt(3);
  ASSERT_TRUE(web_state);
  EXPECT_EQ(web_state->GetBrowserState(), browser_state_.get());
  EXPECT_FALSE(web_state->HasOpener());
  // The URL of pending item (i.e. kChromeUINewTabURL) will not be returned
  // here because WebState doesn't load the URL until it's visible and
  // NavigationManager::GetVisibleURL requires WebState::IsLoading to be true
  // to return pending item's URL.
  EXPECT_EQ("", web_state->GetVisibleURL().spec());
  web::WebStateID identifier = web_state->GetUniqueIdentifier();
  EXPECT_FALSE(base::Contains(original_identifiers_, identifier));
  // Consumer checks.
  EXPECT_EQ(4UL, consumer_.items.size());
  EXPECT_EQ(identifier, consumer_.selectedItem.tabSwitcherItem.identifier);
  EXPECT_EQ(identifier, consumer_.items[3]);
}

// Tests that `-addNewItem` is a no-op if the mediator's browser
// is nullptr.
TEST_P(BaseGridMediatorTest, AddNewItemWithNoBrowserCommand) {
  mediator_.browser = nullptr;
  ASSERT_EQ(3, browser_->GetWebStateList()->count());
  ASSERT_EQ(1, browser_->GetWebStateList()->active_index());
  [mediator_ addNewItem];
  EXPECT_EQ(3, browser_->GetWebStateList()->count());
  EXPECT_EQ(1, browser_->GetWebStateList()->active_index());
}

// Tests that when `-searchItemsWithText:` is called, there is no change in the
// items in WebStateList and the correct items are populated by the consumer.
TEST_P(BaseGridMediatorTest, SearchItemsWithTextCommand) {
  // Capture ordered original IDs.
  std::vector<web::WebStateID> pre_search_ids;
  for (int i = 0; i < 3; i++) {
    web::WebState* web_state = browser_->GetWebStateList()->GetWebStateAt(i);
    pre_search_ids.push_back(web_state->GetUniqueIdentifier());
  }
  web::WebStateID expected_result_identifier =
      browser_->GetWebStateList()->GetWebStateAt(2)->GetUniqueIdentifier();

  [mediator_ searchItemsWithText:@"hello"];

  // Only one result should be found.
  EXPECT_TRUE(WaitForConsumerUpdates(1UL));
  EXPECT_EQ(expected_result_identifier, consumer_.items[0]);

  // Web states count should not change.
  EXPECT_EQ(3, browser_->GetWebStateList()->count());
  // Active index should not change.
  EXPECT_EQ(1, browser_->GetWebStateList()->active_index());
  // The order of the items should be the same.
  for (int i = 0; i < 3; i++) {
    web::WebState* web_state = browser_->GetWebStateList()->GetWebStateAt(i);
    ASSERT_TRUE(web_state);
    web::WebStateID identifier = web_state->GetUniqueIdentifier();
    EXPECT_EQ(identifier, pre_search_ids[i]);
  }
}

// Tests that when `-resetToAllItems:` is called, the consumer gets all the
// items from items in WebStateList and correct item selected.
TEST_P(BaseGridMediatorTest, resetToAllItems) {
  ASSERT_EQ(3, browser_->GetWebStateList()->count());
  ASSERT_EQ(3UL, consumer_.items.size());

  [mediator_ searchItemsWithText:@"hello"];
  // Only 1 result is in the consumer after the search is done.
  ASSERT_TRUE(WaitForConsumerUpdates(1UL));

  [mediator_ resetToAllItems];
  // consumer should revert back to have the items from the webstate list.
  ASSERT_TRUE(WaitForConsumerUpdates(3UL));
  // Active index should not change.
  EXPECT_EQ(original_selected_identifier_,
            consumer_.selectedItem.tabSwitcherItem.identifier);

  // The order of the items on the consumer be the exact same order as the in
  // WebStateList.
  for (int i = 0; i < 3; i++) {
    web::WebState* web_state = browser_->GetWebStateList()->GetWebStateAt(i);
    ASSERT_TRUE(web_state);
    web::WebStateID identifier = web_state->GetUniqueIdentifier();
    EXPECT_EQ(identifier, consumer_.items[i]);
  }
}

TEST_P(BaseGridMediatorWithPriceDropIndicatorsTest,
       TestSelectItemWithNoPriceDrop) {
  web::WebState* web_state_to_select =
      browser_->GetWebStateList()->GetWebStateAt(2);
  // No need to set a null price drop - it will be null by default.
  [mediator_ selectItemWithID:web_state_to_select->GetUniqueIdentifier()
                       pinned:NO
       isFirstActionOnTabGrid:NO];
  EXPECT_EQ(1, user_action_tester_.GetActionCount(kHasNoPriceDropUserAction));
  EXPECT_EQ(0, user_action_tester_.GetActionCount(kHasPriceDropUserAction));
}

TEST_P(BaseGridMediatorWithPriceDropIndicatorsTest,
       TestSelectItemWithPriceDrop) {
  web::WebState* web_state_to_select =
      browser_->GetWebStateList()->GetWebStateAt(2);
  // Add a fake price drop.
  SetFakePriceDrop(web_state_to_select);
  [mediator_ selectItemWithID:web_state_to_select->GetUniqueIdentifier()
                       pinned:NO
       isFirstActionOnTabGrid:NO];
  EXPECT_EQ(1, user_action_tester_.GetActionCount(kHasPriceDropUserAction));
  EXPECT_EQ(0, user_action_tester_.GetActionCount(kHasNoPriceDropUserAction));
}

// Ensures that when there is web states in normal mode, the toolbar
// configuration is correct.
TEST_P(BaseGridMediatorTest, TestToolbarsNormalModeWithWebstates) {
  EXPECT_EQ(3UL, consumer_.items.size());
  // Force the toolbar configuration by setting the view as currently selected.
  [mediator_ currentlySelectedGrid:YES];
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.closeAllButton);
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.doneButton);
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.newTabButton);
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.searchButton);
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.selectTabsButton);

  EXPECT_FALSE(fake_toolbars_mediator_.configuration.undoButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.deselectAllButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.selectAllButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.addToButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.closeSelectedTabsButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.shareButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.cancelSearchButton);
}

// Ensures that selection button are correctly enabled when pushing select tab
// button.
TEST_P(BaseGridMediatorTest, TestToolbarsSelectionModeWithoutSelection) {
  EXPECT_EQ(3UL, consumer_.items.size());
  [mediator_ selectTabsButtonTapped:nil];

  EXPECT_TRUE(fake_toolbars_mediator_.configuration.selectAllButton);
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.doneButton);
  EXPECT_EQ(0u, fake_toolbars_mediator_.configuration.selectedItemsCount);

  EXPECT_FALSE(fake_toolbars_mediator_.configuration.closeAllButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.newTabButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.searchButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.selectTabsButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.undoButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.deselectAllButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.addToButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.closeSelectedTabsButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.shareButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.cancelSearchButton);
}

// Ensures that selection button are correctly enabled when pushing select tab
// button and the user selected one tab.
TEST_P(BaseGridMediatorTest, TestToolbarsSelectionModeWithSelection) {
  EXPECT_EQ(3UL, consumer_.items.size());
  [mediator_ selectTabsButtonTapped:nil];

  // Simulate a user who tapped on a tab.
  [mediator_ userTappedOnItemID:[GridItemIdentifier
                                    tabIdentifier:browser_->GetWebStateList()
                                                      ->GetWebStateAt(1)]];

  EXPECT_TRUE(fake_toolbars_mediator_.configuration.selectAllButton);
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.doneButton);
  EXPECT_EQ(1u, fake_toolbars_mediator_.configuration.selectedItemsCount);
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.closeSelectedTabsButton);
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.shareButton);
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.addToButton);

  EXPECT_FALSE(fake_toolbars_mediator_.configuration.closeAllButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.newTabButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.searchButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.selectTabsButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.undoButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.deselectAllButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.cancelSearchButton);
}

// Tests that no updates to the toolbars happen when the mediator is not
// selected.
TEST_P(BaseGridMediatorTest, NoToolbarUpdateNotSelected) {
  EXPECT_EQ(3UL, consumer_.items.size());
  [mediator_ selectTabsButtonTapped:nil];

  EXPECT_TRUE(fake_toolbars_mediator_.configuration.selectAllButton);
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.doneButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.closeSelectedTabsButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.shareButton);

  [mediator_ currentlySelectedGrid:NO];

  // Simulate a user who tapped on a tab.
  [mediator_ userTappedOnItemID:[GridItemIdentifier
                                    tabIdentifier:browser_->GetWebStateList()
                                                      ->GetWebStateAt(1)]];

  // No update on the configuration as the mediator is no longer selected.
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.selectAllButton);
  EXPECT_TRUE(fake_toolbars_mediator_.configuration.doneButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.closeSelectedTabsButton);
  EXPECT_FALSE(fake_toolbars_mediator_.configuration.shareButton);
}

// Tests selecting a NTP with no existing groups. The option to add to a group
// should be presented, the others would be disabled.
TEST_P(BaseGridMediatorTest, NTPSelectedWithoutGroup) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip}, {});

  ASSERT_EQ(3UL, consumer_.items.size());
  browser_->GetWebStateList()->InsertWebState(
      CreateFakeWebStateWithURL(GURL("about:newtab")));
  ASSERT_EQ(4UL, consumer_.items.size());

  [mediator_ selectTabsButtonTapped:nil];

  // Simulate a user who tapped on the NTP.
  [mediator_ userTappedOnItemID:[GridItemIdentifier
                                    tabIdentifier:browser_->GetWebStateList()
                                                      ->GetWebStateAt(3)]];

  TabGridToolbarsConfiguration* configuration =
      fake_toolbars_mediator_.configuration;
  EXPECT_TRUE(configuration.selectAllButton);
  EXPECT_TRUE(configuration.doneButton);
  EXPECT_EQ(1u, configuration.selectedItemsCount);
  EXPECT_TRUE(configuration.closeSelectedTabsButton);
  EXPECT_TRUE(configuration.addToButton);

  EXPECT_FALSE(configuration.shareButton);
  EXPECT_FALSE(configuration.closeAllButton);
  EXPECT_FALSE(configuration.newTabButton);
  EXPECT_FALSE(configuration.searchButton);
  EXPECT_FALSE(configuration.selectTabsButton);
  EXPECT_FALSE(configuration.undoButton);
  EXPECT_FALSE(configuration.deselectAllButton);
  EXPECT_FALSE(configuration.cancelSearchButton);

  ASSERT_EQ(3u, configuration.addToButtonMenu.children.count);
  // Add to bookmark/reading list are disabled as it is a NTP.
  UIMenuElement* addToBookmark = configuration.addToButtonMenu.children[1];
  EXPECT_EQ(UIMenuElementAttributesDisabled,
            base::apple::ObjCCast<UIAction>(addToBookmark).attributes);
  UIMenuElement* addToReadingList = configuration.addToButtonMenu.children[1];
  EXPECT_EQ(UIMenuElementAttributesDisabled,
            base::apple::ObjCCast<UIAction>(addToReadingList).attributes);

  // Even if there is a single option, it is displayed as an inlined menu.
  UIMenuElement* addToGroupElement = configuration.addToButtonMenu.children[0];
  ASSERT_TRUE([addToGroupElement isKindOfClass:UIMenu.class]);

  UIMenu* addToGroupMenu = base::apple::ObjCCast<UIMenu>(addToGroupElement);
  ASSERT_EQ(1u, addToGroupMenu.children.count);

  UIMenuElement* addToGroup = addToGroupMenu.children[0];
  EXPECT_TRUE([addToGroup isKindOfClass:UIAction.class]);

  EXPECT_NSEQ(l10n_util::GetPluralNSStringF(
                  IDS_IOS_CONTENT_CONTEXT_ADDTABTONEWTABGROUP, 1),
              addToGroup.title);
}

// Tests selecting a tab with one existing group.
TEST_P(BaseGridMediatorTest, SelectedTabWithGroup) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip}, {});

  EXPECT_EQ(3UL, consumer_.items.size());

  [mediator_ selectTabsButtonTapped:nil];
  browser_->GetWebStateList()->CreateGroup(
      {1},
      tab_groups::TabGroupVisualData(u"My group",
                                     tab_groups::TabGroupColorId::kBlue),
      TabGroupId::GenerateNew());

  // Simulate a user who tapped on a tab.
  [mediator_ userTappedOnItemID:[GridItemIdentifier
                                    tabIdentifier:browser_->GetWebStateList()
                                                      ->GetWebStateAt(2)]];

  TabGridToolbarsConfiguration* configuration =
      fake_toolbars_mediator_.configuration;
  EXPECT_TRUE(configuration.selectAllButton);
  EXPECT_TRUE(configuration.doneButton);
  EXPECT_EQ(1u, configuration.selectedItemsCount);
  EXPECT_TRUE(configuration.closeSelectedTabsButton);
  EXPECT_TRUE(configuration.addToButton);
  EXPECT_TRUE(configuration.shareButton);

  EXPECT_FALSE(configuration.closeAllButton);
  EXPECT_FALSE(configuration.newTabButton);
  EXPECT_FALSE(configuration.searchButton);
  EXPECT_FALSE(configuration.selectTabsButton);
  EXPECT_FALSE(configuration.undoButton);
  EXPECT_FALSE(configuration.deselectAllButton);
  EXPECT_FALSE(configuration.cancelSearchButton);

  ASSERT_EQ(3u, configuration.addToButtonMenu.children.count);
  // Even if there is a single option, it is displayed as an inlined menu.
  UIMenuElement* addToGroupElement = configuration.addToButtonMenu.children[0];
  ASSERT_TRUE([addToGroupElement isKindOfClass:UIMenu.class]);

  UIMenu* addToGroupMenu = base::apple::ObjCCast<UIMenu>(addToGroupElement);
  ASSERT_EQ(1u, addToGroupMenu.children.count);

  UIMenuElement* addToGroupSubmenuElement = addToGroupMenu.children[0];
  ASSERT_TRUE([addToGroupSubmenuElement isKindOfClass:UIMenu.class]);
  UIMenu* addToGroupSubmenu =
      base::apple::ObjCCast<UIMenu>(addToGroupSubmenuElement);
  EXPECT_NSEQ(l10n_util::GetPluralNSStringF(
                  IDS_IOS_CONTENT_CONTEXT_ADDTABTOTABGROUP, 1),
              addToGroupSubmenu.title);
  ASSERT_EQ(2u, addToGroupSubmenu.children.count);

  UIMenuElement* addToGroup = addToGroupSubmenu.children[0];
  EXPECT_TRUE([addToGroup isKindOfClass:UIAction.class]);
  EXPECT_NSEQ(l10n_util::GetNSString(
                  IDS_IOS_CONTENT_CONTEXT_ADDTABTONEWTABGROUP_SUBMENU),
              addToGroup.title);

  UIMenuElement* groupsElement = addToGroupSubmenu.children[1];
  ASSERT_TRUE([groupsElement isKindOfClass:UIMenu.class]);
  UIMenu* groups = base::apple::ObjCCast<UIMenu>(groupsElement);
  EXPECT_EQ(1u, groups.children.count);
  EXPECT_NSEQ(@"My group", groups.children[0].title);
}

// Tests that closing all tabs then adding a tab to the WebStateList removes the
// undo.
TEST_P(BaseGridMediatorTest, CloseAllThenAddWebState) {
  EXPECT_EQ(3UL, consumer_.items.size());
  [mediator_ closeAllButtonTapped:nil];

  TabGridToolbarsConfiguration* configuration =
      fake_toolbars_mediator_.configuration;
  EXPECT_TRUE(configuration.newTabButton);
  EXPECT_TRUE(configuration.searchButton);
  if (GetParam() == TEST_REGULAR_MEDIATOR) {
    // Undo is only available in regular.
    EXPECT_TRUE(configuration.undoButton);
  } else {
    EXPECT_FALSE(configuration.undoButton);
  }

  EXPECT_FALSE(configuration.selectAllButton);
  EXPECT_FALSE(configuration.doneButton);
  EXPECT_EQ(0u, configuration.selectedItemsCount);
  EXPECT_FALSE(configuration.closeSelectedTabsButton);
  EXPECT_FALSE(configuration.addToButton);
  EXPECT_FALSE(configuration.shareButton);
  EXPECT_FALSE(configuration.closeAllButton);
  EXPECT_FALSE(configuration.selectTabsButton);
  EXPECT_FALSE(configuration.deselectAllButton);
  EXPECT_FALSE(configuration.cancelSearchButton);

  // Insert a WebState, the undo should be gone.
  std::unique_ptr<web::FakeWebState> web_state =
      CreateFakeWebStateWithURL(GURL("http://google.com"));
  browser_->GetWebStateList()->InsertWebState(std::move(web_state));

  configuration = fake_toolbars_mediator_.configuration;
  EXPECT_TRUE(configuration.closeAllButton);
  EXPECT_TRUE(configuration.doneButton);
  EXPECT_TRUE(configuration.newTabButton);
  EXPECT_TRUE(configuration.searchButton);
  EXPECT_TRUE(configuration.selectTabsButton);

  EXPECT_FALSE(configuration.undoButton);
  EXPECT_FALSE(configuration.deselectAllButton);
  EXPECT_FALSE(configuration.selectAllButton);
  EXPECT_FALSE(configuration.addToButton);
  EXPECT_FALSE(configuration.closeSelectedTabsButton);
  EXPECT_FALSE(configuration.shareButton);
  EXPECT_FALSE(configuration.cancelSearchButton);
}

// Tests selecting a tab and a group with one existing group.
TEST_P(BaseGridMediatorTest, SelectedTabAndGroupWithGroup) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip}, {});

  EXPECT_EQ(3UL, consumer_.items.size());

  [mediator_ selectTabsButtonTapped:nil];
  browser_->GetWebStateList()->CreateGroup(
      {1},
      tab_groups::TabGroupVisualData(u"My group",
                                     tab_groups::TabGroupColorId::kBlue),
      TabGroupId::GenerateNew());

  // Simulate a user who tapped on a tab and on the group.
  [mediator_ userTappedOnItemID:[GridItemIdentifier
                                    tabIdentifier:browser_->GetWebStateList()
                                                      ->GetWebStateAt(2)]];
  [mediator_ userTappedOnItemID:[GridItemIdentifier
                                    tabIdentifier:browser_->GetWebStateList()
                                                      ->GetWebStateAt(1)]];

  TabGridToolbarsConfiguration* configuration =
      fake_toolbars_mediator_.configuration;
  EXPECT_TRUE(configuration.selectAllButton);
  EXPECT_TRUE(configuration.doneButton);
  EXPECT_EQ(2u, configuration.selectedItemsCount);
  EXPECT_TRUE(configuration.closeSelectedTabsButton);
  EXPECT_TRUE(configuration.addToButton);
  EXPECT_TRUE(configuration.shareButton);

  EXPECT_FALSE(configuration.closeAllButton);
  EXPECT_FALSE(configuration.newTabButton);
  EXPECT_FALSE(configuration.searchButton);
  EXPECT_FALSE(configuration.selectTabsButton);
  EXPECT_FALSE(configuration.undoButton);
  EXPECT_FALSE(configuration.deselectAllButton);
  EXPECT_FALSE(configuration.cancelSearchButton);

  ASSERT_EQ(3u, configuration.addToButtonMenu.children.count);
  // Even if there is a single option, it is displayed as an inlined menu.
  UIMenuElement* addToGroupElement = configuration.addToButtonMenu.children[0];
  ASSERT_TRUE([addToGroupElement isKindOfClass:UIMenu.class]);

  UIMenu* addToGroupMenu = base::apple::ObjCCast<UIMenu>(addToGroupElement);
  ASSERT_EQ(1u, addToGroupMenu.children.count);

  UIMenuElement* addToGroupSubmenuElement = addToGroupMenu.children[0];
  ASSERT_TRUE([addToGroupSubmenuElement isKindOfClass:UIMenu.class]);
  UIMenu* addToGroupSubmenu =
      base::apple::ObjCCast<UIMenu>(addToGroupSubmenuElement);
  EXPECT_NSEQ(l10n_util::GetPluralNSStringF(
                  IDS_IOS_CONTENT_CONTEXT_ADDTABTOTABGROUP, 2),
              addToGroupSubmenu.title);
  ASSERT_EQ(2u, addToGroupSubmenu.children.count);

  UIMenuElement* addToGroup = addToGroupSubmenu.children[0];
  EXPECT_TRUE([addToGroup isKindOfClass:UIAction.class]);
  EXPECT_NSEQ(l10n_util::GetNSString(
                  IDS_IOS_CONTENT_CONTEXT_ADDTABTONEWTABGROUP_SUBMENU),
              addToGroup.title);

  UIMenuElement* groupsElement = addToGroupSubmenu.children[1];
  ASSERT_TRUE([groupsElement isKindOfClass:UIMenu.class]);
  UIMenu* groups = base::apple::ObjCCast<UIMenu>(groupsElement);
  EXPECT_EQ(1u, groups.children.count);
  EXPECT_NSEQ(@"My group", groups.children[0].title);
}

// Tests that ungrouping a group correctly deletes the group.
TEST_P(BaseGridMediatorTest, UnGroup) {
  scoped_feature_list_.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip, kTabGroupSync}, {});

  tab_groups::MockTabGroupSyncService* mock_service =
      static_cast<tab_groups::MockTabGroupSyncService*>(
          tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
              browser_state_.get()));

  WebStateList* web_state_list = browser_->GetWebStateList();
  TabGroupId tab_group_id = TabGroupId::GenerateNew();
  web_state_list->CreateGroup({1}, {}, tab_group_id);
  const TabGroup* group = web_state_list->GetGroupOfWebStateAt(1);
  EXPECT_EQ(1u, web_state_list->GetGroups().size());
  EXPECT_EQ(3, web_state_list->count());

  EXPECT_CALL(*mock_service, RemoveLocalTabGroupMapping(tab_group_id)).Times(0);

  [mediator_ ungroupTabGroup:group];
  EXPECT_EQ(0u, web_state_list->GetGroups().size());
  EXPECT_EQ(3, web_state_list->count());
}

// Tests that ungrouping a group from another browser (e.g from Search)
// correctly deletes the group.
TEST_P(BaseGridMediatorTest, UnGroupFromAnotherBrowser) {
  scoped_feature_list_.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip, kTabGroupSync}, {});
  mode_holder_.mode = TabGridMode::kSearch;

  WebStateList* other_web_state_list = other_browser_->GetWebStateList();
  WebStateListBuilderFromDescription builder(other_web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| a b c d e f g", other_browser_->GetBrowserState()));

  tab_groups::MockTabGroupSyncService* mock_service =
      static_cast<tab_groups::MockTabGroupSyncService*>(
          tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
              browser_state_.get()));
  TabGroupId tab_group_id = TabGroupId::GenerateNew();
  other_web_state_list->CreateGroup({1}, {}, tab_group_id);
  const TabGroup* group = other_web_state_list->GetGroupOfWebStateAt(1);
  EXPECT_EQ(1u, other_web_state_list->GetGroups().size());
  EXPECT_EQ(7, other_web_state_list->count());

  EXPECT_CALL(*mock_service, RemoveLocalTabGroupMapping(tab_group_id)).Times(0);

  [mediator_ ungroupTabGroup:group];
  EXPECT_EQ(0u, other_web_state_list->GetGroups().size());
  EXPECT_EQ(7, other_web_state_list->count());
}

// Tests that closing the last tab of a selected group clears the selection.
TEST_P(BaseGridMediatorTest, CloseSelectedGroup) {
  scoped_feature_list_.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip, kTabGroupSync}, {});

  tab_groups::MockTabGroupSyncService* mock_service =
      static_cast<tab_groups::MockTabGroupSyncService*>(
          tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
              browser_state_.get()));

  TabGroupId tab_group_id = TabGroupId::GenerateNew();
  WebStateList* web_state_list = browser_->GetWebStateList();
  const TabGroup* group = web_state_list->CreateGroup({1}, {}, tab_group_id);
  mode_holder_.mode = TabGridMode::kSelection;
  [mediator_
      addToSelectionItemID:[GridItemIdentifier groupIdentifier:group
                                              withWebStateList:web_state_list]];
  EXPECT_EQ(1UL, [mediator_ allSelectedDragItems].count);

  EXPECT_CALL(*mock_service, GetGroup(tab_group_id))
      .WillOnce(testing::Return(TestSavedGroup()));
  EXPECT_CALL(*mock_service, RemoveLocalTabGroupMapping(tab_group_id));
  EXPECT_CALL(*mock_service, RemoveGroup(tab_group_id)).Times(0);

  [mediator_ closeItemsWithTabIDs:{} groupIDs:{tab_group_id} tabCount:1];

  EXPECT_EQ(0UL, [mediator_ allSelectedDragItems].count);
}

// Tests that closing a group locally removes the mapping from the sync service.
TEST_P(BaseGridMediatorTest, CloseGroupLocally) {
  scoped_feature_list_.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip, kTabGroupSync}, {});

  tab_groups::MockTabGroupSyncService* mock_service =
      static_cast<tab_groups::MockTabGroupSyncService*>(
          tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
              browser_state_.get()));

  WebStateList* web_state_list = browser_->GetWebStateList();
  TabGroupId tab_group_id = TabGroupId::GenerateNew();
  web_state_list->CreateGroup({1}, {}, tab_group_id);
  const TabGroup* group = web_state_list->GetGroupOfWebStateAt(1);
  EXPECT_EQ(1u, web_state_list->GetGroups().size());

  EXPECT_CALL(*mock_service, GetGroup(tab_group_id))
      .WillOnce(testing::Return(TestSavedGroup()));
  EXPECT_CALL(*mock_service, RemoveLocalTabGroupMapping(tab_group_id));
  EXPECT_CALL(*mock_service, RemoveGroup(tab_group_id)).Times(0);

  [mediator_ closeItemWithIdentifier:[GridItemIdentifier
                                          groupIdentifier:group
                                         withWebStateList:web_state_list]];
  EXPECT_EQ(0u, web_state_list->GetGroups().size());
}

// Tests that closing a group locally from another browser (e.g from Search)
// correctly closes the group and removes the mapping from the sync service.
TEST_P(BaseGridMediatorTest, CloseGroupFromAnotherBrowser) {
  scoped_feature_list_.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip, kTabGroupSync}, {});
  mode_holder_.mode = TabGridMode::kSearch;

  WebStateList* other_web_state_list = other_browser_->GetWebStateList();
  WebStateListBuilderFromDescription builder(other_web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| a b c d e f g", other_browser_->GetBrowserState()));

  tab_groups::MockTabGroupSyncService* mock_service =
      static_cast<tab_groups::MockTabGroupSyncService*>(
          tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
              browser_state_.get()));
  TabGroupId tab_group_id = TabGroupId::GenerateNew();
  const TabGroup* group =
      other_web_state_list->CreateGroup({1}, {}, tab_group_id);
  EXPECT_EQ(1u, other_web_state_list->GetGroups().size());

  EXPECT_CALL(*mock_service, GetGroup(tab_group_id))
      .WillOnce(testing::Return(TestSavedGroup()));
  EXPECT_CALL(*mock_service, RemoveLocalTabGroupMapping(tab_group_id));
  EXPECT_CALL(*mock_service, RemoveGroup(tab_group_id)).Times(0);

  [mediator_
      closeItemWithIdentifier:[GridItemIdentifier
                                   groupIdentifier:group
                                  withWebStateList:other_web_state_list]];
  EXPECT_EQ(0u, other_web_state_list->GetGroups().size());
}

// Tests that closing multiple selected items doesn't delete saved groups.
TEST_P(BaseGridMediatorTest, CloseSelectedTabsAndGroups) {
  scoped_feature_list_.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip, kTabGroupSync}, {});

  WebStateList* web_state_list = browser_->GetWebStateList();
  CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);
  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| a b c [ 1 d e ] [ 2 f g ] h", browser_->GetBrowserState()));

  tab_groups::MockTabGroupSyncService* mock_service =
      static_cast<tab_groups::MockTabGroupSyncService*>(
          tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
              browser_state_.get()));

  const TabGroup* group_1 = builder.GetTabGroupForIdentifier('1');
  const TabGroup* group_2 = builder.GetTabGroupForIdentifier('2');
  TabGroupId tab_group_id_1 = group_1->tab_group_id();
  TabGroupId tab_group_id_2 = group_2->tab_group_id();
  web::WebState* web_state_a = builder.GetWebStateForIdentifier('a');
  web::WebState* web_state_b = builder.GetWebStateForIdentifier('b');

  mode_holder_.mode = TabGridMode::kSelection;
  [mediator_
      addToSelectionItemID:[GridItemIdentifier tabIdentifier:web_state_a]];
  [mediator_
      addToSelectionItemID:[GridItemIdentifier tabIdentifier:web_state_b]];
  [mediator_
      addToSelectionItemID:[GridItemIdentifier groupIdentifier:group_1
                                              withWebStateList:web_state_list]];
  [mediator_
      addToSelectionItemID:[GridItemIdentifier groupIdentifier:group_2
                                              withWebStateList:web_state_list]];

  // 2 tabs, 2 tab groups.
  EXPECT_EQ(4UL, [mediator_ allSelectedDragItems].count);

  EXPECT_CALL(*mock_service, GetGroup(tab_group_id_1))
      .WillOnce(testing::Return(TestSavedGroup()));
  EXPECT_CALL(*mock_service, GetGroup(tab_group_id_2))
      .WillOnce(testing::Return(TestSavedGroup()));
  EXPECT_CALL(*mock_service, RemoveLocalTabGroupMapping(tab_group_id_1));
  EXPECT_CALL(*mock_service, RemoveLocalTabGroupMapping(tab_group_id_2));
  EXPECT_CALL(*mock_service, RemoveGroup(tab_group_id_1)).Times(0);
  EXPECT_CALL(*mock_service, RemoveGroup(tab_group_id_2)).Times(0);

  [mediator_ closeItemsWithTabIDs:{web_state_a->GetUniqueIdentifier(),
                                   web_state_b->GetUniqueIdentifier()}
                         groupIDs:{tab_group_id_1, tab_group_id_2}
                         tabCount:6];

  ASSERT_EQ("| c h", builder.GetWebStateListDescription());
  EXPECT_EQ(0UL, [mediator_ allSelectedDragItems].count);
}

// Tests that closing the last tab of a selected group in a batch operation
// clears the selection.
TEST_P(BaseGridMediatorTest, CloseSelectedGroupInBatch) {
  WebStateList* web_state_list = browser_->GetWebStateList();
  web_state_list->CreateGroup({1}, {}, TabGroupId::GenerateNew());
  const TabGroup* group = web_state_list->GetGroupOfWebStateAt(1);
  mode_holder_.mode = TabGridMode::kSelection;
  [mediator_
      addToSelectionItemID:[GridItemIdentifier groupIdentifier:group
                                              withWebStateList:web_state_list]];
  EXPECT_EQ(1UL, [mediator_ allSelectedDragItems].count);

  {
    WebStateList::ScopedBatchOperation lock =
        browser_->GetWebStateList()->StartBatchOperation();
    browser_->GetWebStateList()->CloseWebStateAt(
        1, WebStateList::CLOSE_USER_ACTION);
  }

  EXPECT_EQ(0UL, [mediator_ allSelectedDragItems].count);
}

// Tests that moving the selected tab from one group to another correctly
// updates the selected element of the Grid, whether the tab itself is moving in
// the web state list or not.
TEST_P(BaseGridMediatorTest, SelectionAfterChangingGroup) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip}, {});

  WebStateList* web_state_list = browser_->GetWebStateList();
  CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);
  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| [ 1 a* b ] [ 2 c ]", browser_->GetBrowserState()));
  const TabGroup* group1 = builder.GetTabGroupForIdentifier('1');
  const TabGroup* group2 = builder.GetTabGroupForIdentifier('2');

  ASSERT_EQ(group1, consumer_.selectedItem.tabGroupItem.tabGroup);

  // Check after a move.
  web_state_list->MoveToGroup({0}, group2);
  ASSERT_EQ("| [ 1 b ] [ 2 c a* ]", builder.GetWebStateListDescription());
  EXPECT_EQ(group2, consumer_.selectedItem.tabGroupItem.tabGroup);

  // Check after a status-only change.
  web_state_list->ActivateWebStateAt(1);
  web_state_list->MoveToGroup({1}, group1);
  ASSERT_EQ("| [ 1 b c* ] [ 2 a ]", builder.GetWebStateListDescription());
  EXPECT_EQ(group1, consumer_.selectedItem.tabGroupItem.tabGroup);
}

// Tests dropping a local tab (e.g. drag from same window) in the grid.
TEST_P(BaseGridMediatorTest, DropLocalTab) {
  WebStateList* web_state_list = browser_->GetWebStateList();
  CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);

  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| a* b c ", browser_->GetBrowserState()));

  web::WebStateID web_state_id =
      web_state_list->GetWebStateAt(2)->GetUniqueIdentifier();

  id local_object = [[TabInfo alloc] initWithTabID:web_state_id
                                      browserState:browser_->GetBrowserState()];
  NSItemProvider* item_provider = [[NSItemProvider alloc] init];
  UIDragItem* drag_item =
      [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;

  // Drop item.
  [mediator_ dropItem:drag_item toIndex:0 fromSameCollection:YES];

  EXPECT_EQ("| c a* b", builder.GetWebStateListDescription());
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameCollection);
}

// Tests dropping a tabs from the tab group view in the grid.
TEST_P(BaseGridMediatorTest, DropLocalTabFromTabGroup) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip}, {});

  WebStateList* web_state_list = browser_->GetWebStateList();
  CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);

  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| a* b c [ 0 d e f ] g", browser_->GetBrowserState()));

  // Drop "D" (in a group) after "A".
  web::WebStateID web_state_id =
      web_state_list->GetWebStateAt(3)->GetUniqueIdentifier();
  id local_object = [[TabInfo alloc] initWithTabID:web_state_id
                                      browserState:browser_->GetBrowserState()];
  NSItemProvider* item_provider = [[NSItemProvider alloc] init];
  UIDragItem* drag_item =
      [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;
  [mediator_ dropItem:drag_item toIndex:1 fromSameCollection:NO];
  EXPECT_EQ("| a* d b c [ 0 e f ] g", builder.GetWebStateListDescription());
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameBrowser, 1);

  // Drop "E" (in a group) before "G".
  web_state_id = web_state_list->GetWebStateAt(4)->GetUniqueIdentifier();
  local_object = [[TabInfo alloc] initWithTabID:web_state_id
                                   browserState:browser_->GetBrowserState()];
  item_provider = [[NSItemProvider alloc] init];
  drag_item = [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;
  [mediator_ dropItem:drag_item toIndex:5 fromSameCollection:NO];
  EXPECT_EQ("| a* d b c [ 0 f ] e g", builder.GetWebStateListDescription());
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameBrowser, 2);
}

// Tests dropping a tab from another browser (e.g. drag from another window) in
// the grid.
TEST_P(BaseGridMediatorTest, DropCrossWindowTab) {
  auto other_browser = std::make_unique<TestBrowser>(
      browser_state_.get(), scene_state_,
      std::make_unique<BrowserWebStateListDelegate>());
  SnapshotBrowserAgent::CreateForBrowser(other_browser.get());

  browser_list_->AddBrowser(other_browser.get());

  GURL url_to_load = GURL(kDraggedUrl);
  std::unique_ptr<web::FakeWebState> other_web_state =
      CreateFakeWebStateWithURL(url_to_load);
  web::WebStateID other_id = other_web_state->GetUniqueIdentifier();
  other_browser->GetWebStateList()->InsertWebState(std::move(other_web_state));

  WebStateList* web_state_list = browser_->GetWebStateList();
  CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);

  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| a* b c ", browser_->GetBrowserState()));

  id local_object = [[TabInfo alloc] initWithTabID:other_id
                                      browserState:browser_->GetBrowserState()];
  NSItemProvider* item_provider = [[NSItemProvider alloc] init];
  UIDragItem* drag_item =
      [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;

  // Drop item.
  [mediator_ dropItem:drag_item toIndex:1 fromSameCollection:NO];

  EXPECT_EQ(4, web_state_list->count());
  EXPECT_EQ(0, other_browser->GetWebStateList()->count());
  EXPECT_EQ(url_to_load, web_state_list->GetWebStateAt(1)->GetVisibleURL());
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kOtherBrowser);
}

// Tests dropping a local Tab Group (i.e. from the same window).
TEST_P(BaseGridMediatorTest, DropLocalTabGroup) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip}, {});

  WebStateList* web_state_list = browser_->GetWebStateList();
  CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);

  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| [ 1 a* b ] c [ 2 d e ]", browser_->GetBrowserState()));

  const TabGroup* tab_group = web_state_list->GetGroupOfWebStateAt(4);

  id local_object =
      [[TabGroupInfo alloc] initWithTabGroup:tab_group
                                browserState:browser_state_.get()];
  NSItemProvider* item_provider = [[NSItemProvider alloc] init];
  UIDragItem* drag_item =
      [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;

  // Drop item.
  [mediator_ dropItem:drag_item toIndex:1 fromSameCollection:YES];

  EXPECT_EQ("| [ 1 a* b ] [ 2 d e ] c", builder.GetWebStateListDescription());
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameCollection);
}

// Tests dropping a Tab Group from another browser (i.e. from the same window).
TEST_P(BaseGridMediatorTest, DropCrossBrowserTabGroup) {
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip}, {});

  // Prepare the web state list in which the group will be dropped.
  WebStateList* web_state_list = browser_->GetWebStateList();
  CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);

  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| [ 1 a* b ] c [ 2 d e ]", browser_->GetBrowserState()));

  // Prepare the other web state list, from which the group will be dragged.
  auto other_browser = std::make_unique<TestBrowser>(
      browser_state_.get(), scene_state_,
      std::make_unique<BrowserWebStateListDelegate>());
  SnapshotBrowserAgent::CreateForBrowser(other_browser.get());

  browser_list_->AddBrowser(other_browser.get());

  GURL url_to_load = GURL(kDraggedUrl);
  other_browser->GetWebStateList()->InsertWebState(
      CreateFakeWebStateWithURL(url_to_load));
  other_browser->GetWebStateList()->CreateGroup({0}, {},
                                                TabGroupId::GenerateNew());

  const TabGroup* other_tab_group =
      other_browser->GetWebStateList()->GetGroupOfWebStateAt(0);
  tab_groups::TabGroupId tab_group_id = other_tab_group->tab_group_id();

  id local_object =
      [[TabGroupInfo alloc] initWithTabGroup:other_tab_group
                                browserState:browser_state_.get()];
  NSItemProvider* item_provider = [[NSItemProvider alloc] init];
  UIDragItem* drag_item =
      [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;

  // Drop item.
  [mediator_ dropItem:drag_item toIndex:2 fromSameCollection:NO];

  EXPECT_EQ(nullptr, web_state_list->GetGroupOfWebStateAt(2));
  EXPECT_EQ(tab_group_id,
            web_state_list->GetGroupOfWebStateAt(3)->tab_group_id());
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kOtherBrowser);
}

// Tests dropping an interal URL (e.g. drag from omnibox) in the grid.
TEST_P(BaseGridMediatorTest, DropInternalURL) {
  WebStateList* web_state_list = browser_->GetWebStateList();
  CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);
  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| a* b c ", browser_->GetBrowserState()));

  GURL url_to_load = GURL(kDraggedUrl);
  id local_object = [[URLInfo alloc] initWithURL:url_to_load title:@"My title"];
  NSItemProvider* item_provider = [[NSItemProvider alloc] init];
  UIDragItem* drag_item =
      [[UIDragItem alloc] initWithItemProvider:item_provider];
  drag_item.localObject = local_object;

  // Drop item.
  [mediator_ dropItem:drag_item toIndex:1 fromSameCollection:YES];

  EXPECT_EQ(4, web_state_list->count());
  web::WebState* web_state = web_state_list->GetWebStateAt(1);
  EXPECT_EQ(url_to_load,
            web_state->GetNavigationManager()->GetPendingItem()->GetURL());
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kOther);
}

// Tests dropping an external URL in the grid.
TEST_P(BaseGridMediatorTest, DropExternalURL) {
  WebStateList* web_state_list = browser_->GetWebStateList();
  CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);
  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| a* b c ", browser_->GetBrowserState()));

  NSItemProvider* item_provider = [[NSItemProvider alloc]
      initWithContentsOfURL:[NSURL URLWithString:base::SysUTF8ToNSString(
                                                     kDraggedUrl)]];

  // Drop item.
  [mediator_ dropItemFromProvider:item_provider
                          toIndex:1
               placeholderContext:nil];

  EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
      base::Seconds(1), ^bool(void) {
        return web_state_list->count() == 4;
      }));
  web::WebState* web_state = web_state_list->GetWebStateAt(1);
  EXPECT_EQ(GURL(kDraggedUrl),
            web_state->GetNavigationManager()->GetPendingItem()->GetURL());
  ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kOther);
}

INSTANTIATE_TEST_SUITE_P(
    /* No InstantiationName */,
    BaseGridMediatorTest,
    ::testing::Values(GridMediatorType::TEST_REGULAR_MEDIATOR,
                      GridMediatorType::TEST_INCOGNITO_MEDIATOR));

INSTANTIATE_TEST_SUITE_P(
    /* No InstantiationName */,
    BaseGridMediatorWithPriceDropIndicatorsTest,
    ::testing::Values(GridMediatorType::TEST_REGULAR_MEDIATOR,
                      GridMediatorType::TEST_INCOGNITO_MEDIATOR));