chromium/ios/chrome/browser/tabs/model/tabs_closer_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/tabs/model/tabs_closer.h"

#import <optional>

#import "base/functional/bind.h"
#import "base/scoped_observation.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/test_file_util.h"
#import "base/uuid.h"
#import "components/saved_tab_groups/fake_tab_group_sync_service.h"
#import "components/saved_tab_groups/saved_tab_group.h"
#import "components/saved_tab_groups/saved_tab_group_tab.h"
#import "components/sessions/core/tab_restore_service.h"
#import "components/tab_groups/tab_group_color.h"
#import "components/tab_groups/tab_group_id.h"
#import "components/tab_groups/tab_group_visual_data.h"
#import "ios/chrome/browser/saved_tab_groups/model/tab_group_sync_service_factory.h"
#import "ios/chrome/browser/sessions/model/fake_tab_restore_service.h"
#import "ios/chrome/browser/sessions/model/ios_chrome_tab_restore_service_factory.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service_factory.h"
#import "ios/chrome/browser/sessions/model/test_session_restoration_service.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/browser/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_list_observer.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/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/fake_authentication_service_delegate.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.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/test/web_task_environment.h"
#import "ios/web/public/web_state_id.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "ui/base/page_transition_types.h"

using tab_groups::TabGroupVisualData;

namespace {

// Reentrancy observer to help checking the state of the tab closer during the
// operations.
class ReentrancyObserver : public WebStateListObserver {
 public:
  ReentrancyObserver(TabsCloser& tabs_closer) : tabs_closer_(&tabs_closer) {}

  void WebStateListDidChange(WebStateList* web_state_list,
                             const WebStateListChange& change,
                             const WebStateListStatus& status) final {
    can_undo_ = tabs_closer_->CanUndoCloseTabs();
  }

  bool CheckThatUndoCloseTabsWasNotPossible() {
    return can_undo_.has_value() && !can_undo_.value();
  }

 private:
  raw_ptr<TabsCloser> tabs_closer_;
  std::optional<bool> can_undo_ = std::nullopt;
};

// Controls whether the WebState is inserted as pinned or regular.
enum class InsertionPolicy {
  kPinned,
  kRegular,
};

// List all ContentWorlds. Necessary because calling SetWebFramesManager(...)
// with a kAllContentWorlds is not enough with FakeWebState.
constexpr web::ContentWorld kContentWorlds[] = {
    web::ContentWorld::kAllContentWorlds,
    web::ContentWorld::kPageContentWorld,
    web::ContentWorld::kIsolatedWorld,
};

// Session name used by the fake SceneState.
const char kSceneSessionID[] = "Identifier";

// WebStateListObserver checking whether a batch operation has been
// performed (i.e. started and then completed).
class ScopedTestWebStateListObserver final : public WebStateListObserver {
 public:
  ScopedTestWebStateListObserver() = default;

  // Start observing `web_state_list`.
  void Observe(WebStateList* web_state_list) {
    scoped_observation_.Observe(web_state_list);
  }

  // Returns whether a batch operation has been performed (i.e. started and then
  // completed).
  bool BatchOperationCompleted() const {
    return batch_operation_started_ && batch_operation_ended_;
  }

  // WebStateListObserver implementation.
  void WillBeginBatchOperation(WebStateList* web_state_list) final {
    batch_operation_started_ = true;
  }

  void BatchOperationEnded(WebStateList* web_state_list) final {
    batch_operation_ended_ = true;
  }

 private:
  // Records whether a batch operation started/ended.
  bool batch_operation_started_ = false;
  bool batch_operation_ended_ = false;

  // Scoped observation used to unregister itself when the object is destroyed.
  base::ScopedObservation<WebStateList, WebStateListObserver>
      scoped_observation_{this};
};

// Creates a FakeTabGroupSyncService.
std::unique_ptr<KeyedService> CreateFakeTabGroupSyncService(
    web::BrowserState* context) {
  return std::make_unique<tab_groups::FakeTabGroupSyncService>();
}

}  // namespace

class TabsCloserTest : public PlatformTest {
 public:
  TabsCloserTest() {
    // Create a TestChromeBrowserState with required services.
    TestChromeBrowserState::Builder builder;
    builder.AddTestingFactory(
        AuthenticationServiceFactory::GetInstance(),
        AuthenticationServiceFactory::GetDefaultFactory());
    builder.AddTestingFactory(
        SessionRestorationServiceFactory::GetInstance(),
        TestSessionRestorationService::GetTestingFactory());
    builder.AddTestingFactory(IOSChromeTabRestoreServiceFactory::GetInstance(),
                              FakeTabRestoreService::GetTestingFactory());
    builder.AddTestingFactory(
        tab_groups::TabGroupSyncServiceFactory::GetInstance(),
        base::BindRepeating(&CreateFakeTabGroupSyncService));
    browser_state_ = std::move(builder).Build();

    fake_tab_group_service_ = static_cast<tab_groups::FakeTabGroupSyncService*>(
        tab_groups::TabGroupSyncServiceFactory::GetForBrowserState(
            browser_state_.get()));

    // Initialize the AuthenticationService.
    AuthenticationServiceFactory::CreateAndInitializeForBrowserState(
        browser_state_.get(),
        std::make_unique<FakeAuthenticationServiceDelegate>());

    scene_state_ = OCMClassMock([SceneState class]);
    OCMStub([scene_state_ sceneSessionID]).andReturn(@(kSceneSessionID));
    browser_ = Browser::Create(browser_state_.get(), scene_state_);
  }

  Browser* browser() { return browser_.get(); }

  sessions::TabRestoreService* restore_service() {
    return IOSChromeTabRestoreServiceFactory::GetForBrowserState(
        browser_state_.get());
  }

  tab_groups::FakeTabGroupSyncService* tab_group_service() {
    return fake_tab_group_service_;
  }

  // Appends a fake WebState in `browser_` using `policy` and `opener`.
  web::WebState* AppendWebState(InsertionPolicy policy, WebStateOpener opener) {
    const GURL url = GURL("https://example.com");
    auto navigation_manager = std::make_unique<web::FakeNavigationManager>();
    navigation_manager->AddItem(url, ui::PAGE_TRANSITION_LINK);
    navigation_manager->SetLastCommittedItem(
        navigation_manager->GetItemAtIndex(0));

    auto web_state = std::make_unique<web::FakeWebState>();
    web_state->SetIsRealized(true);
    web_state->SetVisibleURL(url);
    web_state->SetBrowserState(browser_->GetBrowserState());
    web_state->SetNavigationManager(std::move(navigation_manager));
    web_state->SetNavigationItemCount(1);

    for (const web::ContentWorld content_world : kContentWorlds) {
      web_state->SetWebFramesManager(
          content_world, std::make_unique<web::FakeWebFramesManager>());
    }

    WebStateList* web_state_list = browser_->GetWebStateList();
    // Force the insertion at the end. Otherwise, the opener will trigger logic
    // to move an inserted WebState close to its opener.
    const WebStateList::InsertionParams params =
        WebStateList::InsertionParams::AtIndex(web_state_list->count())
            .Pinned(policy == InsertionPolicy::kPinned)
            .WithOpener(opener);
    const int insertion_index =
        web_state_list->InsertWebState(std::move(web_state), params);

    return web_state_list->GetWebStateAt(insertion_index);
  }

 private:
  web::WebTaskEnvironment task_environment_;
  IOSChromeScopedTestingLocalState scoped_testing_local_state_;
  std::unique_ptr<ChromeBrowserState> browser_state_;
  __strong SceneState* scene_state_;
  std::unique_ptr<Browser> browser_;
  tab_groups::FakeTabGroupSyncService* fake_tab_group_service_;
};

// Tests how a TabsCloser behaves when presented with a Browser containing
// no tabs.
//
// Variants: ClosePolicy::kAllTabs
TEST_F(TabsCloserTest, EmptyBrowser_ClosePolicyAllTabs) {
  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kAllTabs);
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());
}

// Tests how a TabsCloser behaves when presented with a Browser containing
// no tabs.
//
// Variants: ClosePolicy::kRegularTabs
TEST_F(TabsCloserTest, EmptyBrowser_ClosePolicyRegularTabs) {
  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kRegularTabs);
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());
}

// Tests how a TabsCloser behaves when presented with a Browser containing
// only regular tabs.
//
// Variants: ClosePolicy::kAllTabs
TEST_F(TabsCloserTest, BrowserWithOnlyRegularTabs_ClosePolicyAllTabs) {
  WebStateList* web_state_list = browser()->GetWebStateList();

  web::WebState* web_state0 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{});
  web::WebState* web_state1 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{});
  web::WebState* web_state2 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{web_state1, 0});

  ASSERT_EQ(web_state_list->count(), 3);
  ASSERT_EQ(web_state_list->GetWebStateAt(0), web_state0);
  ASSERT_EQ(web_state_list->GetWebStateAt(1), web_state1);
  ASSERT_EQ(web_state_list->GetWebStateAt(2), web_state2);

  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kAllTabs);

  // Check that some tabs can be closed.
  EXPECT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that calling CloseTabs() closes all the tabs registered, allows to
  // undo the operation, and leaves the WebStateList empty.
  EXPECT_EQ(tabs_closer.CloseTabs(), 3);
  EXPECT_TRUE(tabs_closer.CanUndoCloseTabs());
  EXPECT_TRUE(web_state_list->empty());

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as it has not been confirmed).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling UndoCloseTabs() correctly restores all the closed tabs,
  // in the correct order, and with the opener-opened relationship intact.
  EXPECT_EQ(tabs_closer.UndoCloseTabs(), 3);
  ASSERT_EQ(web_state_list->count(), 3);
  EXPECT_EQ(web_state_list->pinned_tabs_count(), 0);
  EXPECT_EQ(web_state_list->GetWebStateAt(0), web_state0);
  EXPECT_EQ(web_state_list->GetWebStateAt(1), web_state1);
  EXPECT_EQ(web_state_list->GetWebStateAt(2), web_state2);
  EXPECT_EQ(web_state_list->GetOpenerOfWebStateAt(2).opener, web_state1);

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as the operation has been cancelled).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling CloseTabs() and ConfirmDeletion() correctly closes the
  // tabs and prevents undo.
  ASSERT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_EQ(tabs_closer.CloseTabs(), 3);
  EXPECT_EQ(tabs_closer.ConfirmDeletion(), 3);
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that the TabRestoreService has now been informed of the
  // close operation which has been confirmed.
  EXPECT_EQ(restore_service()->entries().size(), 3u);
}

// Tests how a TabsCloser behaves when presented with a Browser containing
// only regular tabs.
//
// Variants: ClosePolicy::kRegularTabs
TEST_F(TabsCloserTest, BrowserWithOnlyRegularTabs) {
  WebStateList* web_state_list = browser()->GetWebStateList();

  web::WebState* web_state0 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{});
  web::WebState* web_state1 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{});
  web::WebState* web_state2 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{web_state1, 0});

  ASSERT_EQ(web_state_list->count(), 3);
  ASSERT_EQ(web_state_list->GetWebStateAt(0), web_state0);
  ASSERT_EQ(web_state_list->GetWebStateAt(1), web_state1);
  ASSERT_EQ(web_state_list->GetWebStateAt(2), web_state2);

  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kRegularTabs);

  // Check that some tabs can be closed.
  EXPECT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that calling CloseTabs() closes all the tabs registered, allows to
  // undo the operation, and leaves the WebStateList empty.
  EXPECT_EQ(tabs_closer.CloseTabs(), 3);
  EXPECT_TRUE(tabs_closer.CanUndoCloseTabs());
  EXPECT_TRUE(web_state_list->empty());

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as it has not been confirmed).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling UndoCloseTabs() correctly restores all the closed tabs,
  // in the correct order, and with the opener-opened relationship intact.
  EXPECT_EQ(tabs_closer.UndoCloseTabs(), 3);
  ASSERT_EQ(web_state_list->count(), 3);
  EXPECT_EQ(web_state_list->pinned_tabs_count(), 0);
  EXPECT_EQ(web_state_list->GetWebStateAt(0), web_state0);
  EXPECT_EQ(web_state_list->GetWebStateAt(1), web_state1);
  EXPECT_EQ(web_state_list->GetWebStateAt(2), web_state2);
  EXPECT_EQ(web_state_list->GetOpenerOfWebStateAt(2).opener, web_state1);

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as the operation has been cancelled).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling CloseTabs() and ConfirmDeletion() correctly closes the
  // tabs and prevents undo.
  ASSERT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_EQ(tabs_closer.CloseTabs(), 3);
  EXPECT_EQ(tabs_closer.ConfirmDeletion(), 3);
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that the TabRestoreService has now been informed of the
  // close operation which has been confirmed.
  EXPECT_EQ(restore_service()->entries().size(), 3u);
}

// Tests how a TabsCloser behaves when presented with a Browser containing
// only pinned tabs.
//
// Variants: ClosePolicy::kAllTabs
TEST_F(TabsCloserTest, BrowserWithOnlyPinnedTabs_ClosePolicyAllTabs) {
  WebStateList* web_state_list = browser()->GetWebStateList();

  web::WebState* web_state0 =
      AppendWebState(InsertionPolicy::kPinned, WebStateOpener{});
  web::WebState* web_state1 =
      AppendWebState(InsertionPolicy::kPinned, WebStateOpener{web_state0, 0});

  ASSERT_EQ(web_state_list->count(), 2);
  ASSERT_EQ(web_state_list->GetWebStateAt(0), web_state0);
  ASSERT_EQ(web_state_list->GetWebStateAt(1), web_state1);

  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kAllTabs);

  // Check that some tabs can be closed.
  EXPECT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that calling CloseTabs() closes all the tabs registered, allows to
  // undo the operation, and leaves the WebStateList empty.
  EXPECT_EQ(tabs_closer.CloseTabs(), 2);
  EXPECT_TRUE(tabs_closer.CanUndoCloseTabs());
  EXPECT_TRUE(web_state_list->empty());

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as it has not been confirmed).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling UndoCloseTabs() correctly restores all the closed tabs,
  // in the correct order, and with the opener-opened relationship intact.
  EXPECT_EQ(tabs_closer.UndoCloseTabs(), 2);
  ASSERT_EQ(web_state_list->count(), 2);
  EXPECT_EQ(web_state_list->pinned_tabs_count(), 2);
  EXPECT_EQ(web_state_list->GetWebStateAt(0), web_state0);
  EXPECT_EQ(web_state_list->GetWebStateAt(1), web_state1);
  EXPECT_EQ(web_state_list->GetOpenerOfWebStateAt(1).opener, web_state0);

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as the operation has been cancelled).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling CloseTabs() and ConfirmDeletion() correctly closes the
  // tabs and prevents undo.
  ASSERT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_EQ(tabs_closer.CloseTabs(), 2);
  EXPECT_EQ(tabs_closer.ConfirmDeletion(), 2);
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that the TabRestoreService has now been informed of the
  // close operation which has been confirmed.
  EXPECT_EQ(restore_service()->entries().size(), 2u);
}

// Tests how a TabsCloser behaves when presented with a Browser containing
// only pinned tabs.
//
// Variants: ClosePolicy::kRegularTabs
TEST_F(TabsCloserTest, BrowserWithOnlyPinnedTabs_ClosePolicyRegularTabs) {
  WebStateList* web_state_list = browser()->GetWebStateList();

  web::WebState* web_state0 =
      AppendWebState(InsertionPolicy::kPinned, WebStateOpener{});
  web::WebState* web_state1 =
      AppendWebState(InsertionPolicy::kPinned, WebStateOpener{web_state0, 0});

  ASSERT_EQ(web_state_list->count(), 2);
  ASSERT_EQ(web_state_list->GetWebStateAt(0), web_state0);
  ASSERT_EQ(web_state_list->GetWebStateAt(1), web_state1);

  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kRegularTabs);

  // Check that nothing can be closed since there are only pinned
  // tabs in the WebStateList.
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());
}

// Tests how a TabsCloser behaves when presented with a Browser containing
// regular and pinned tabs.
//
// Variants: ClosePolicy::kAllTabs
TEST_F(TabsCloserTest, BrowserWithRegularAndPinnedTabs_ClosePolicyAllTabs) {
  WebStateList* web_state_list = browser()->GetWebStateList();

  web::WebState* web_state0 =
      AppendWebState(InsertionPolicy::kPinned, WebStateOpener{});
  web::WebState* web_state1 =
      AppendWebState(InsertionPolicy::kPinned, WebStateOpener{web_state0, 0});
  web::WebState* web_state2 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{});
  web::WebState* web_state3 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{web_state1, 0});
  web::WebState* web_state4 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{web_state3, 0});

  ASSERT_EQ(web_state_list->count(), 5);
  ASSERT_EQ(web_state_list->GetWebStateAt(0), web_state0);
  ASSERT_EQ(web_state_list->GetWebStateAt(1), web_state1);
  ASSERT_EQ(web_state_list->GetWebStateAt(2), web_state2);
  ASSERT_EQ(web_state_list->GetWebStateAt(3), web_state3);
  ASSERT_EQ(web_state_list->GetWebStateAt(4), web_state4);

  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kAllTabs);

  // Check that some tabs can be closed.
  EXPECT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that calling CloseTabs() closes all the tabs registered, allows to
  // undo the operation, and leaves the WebStateList empty.
  EXPECT_EQ(tabs_closer.CloseTabs(), 5);
  EXPECT_TRUE(tabs_closer.CanUndoCloseTabs());
  EXPECT_TRUE(web_state_list->empty());

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as it has not been confirmed).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling UndoCloseTabs() correctly restores all the closed tabs,
  // in the correct order, and with the opener-opened relationship intact.
  EXPECT_EQ(tabs_closer.UndoCloseTabs(), 5);
  ASSERT_EQ(web_state_list->count(), 5);
  EXPECT_EQ(web_state_list->pinned_tabs_count(), 2);
  EXPECT_EQ(web_state_list->GetWebStateAt(0), web_state0);
  EXPECT_EQ(web_state_list->GetWebStateAt(1), web_state1);
  EXPECT_EQ(web_state_list->GetWebStateAt(2), web_state2);
  EXPECT_EQ(web_state_list->GetWebStateAt(3), web_state3);
  EXPECT_EQ(web_state_list->GetWebStateAt(4), web_state4);
  EXPECT_EQ(web_state_list->GetOpenerOfWebStateAt(1).opener, web_state0);
  EXPECT_EQ(web_state_list->GetOpenerOfWebStateAt(3).opener, web_state1);
  EXPECT_EQ(web_state_list->GetOpenerOfWebStateAt(4).opener, web_state3);

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as the operation has been cancelled).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling CloseTabs() and ConfirmDeletion() correctly closes the
  // tabs and prevents undo.
  ASSERT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_EQ(tabs_closer.CloseTabs(), 5);
  EXPECT_EQ(tabs_closer.ConfirmDeletion(), 5);
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that the TabRestoreService has now been informed of the
  // close operation which has been confirmed.
  EXPECT_EQ(restore_service()->entries().size(), 5u);
}

// Tests how a TabsCloser behaves when presented with a Browser containing
// regular and pinned tabs.
//
// Variants: ClosePolicy::kRegularTabs
TEST_F(TabsCloserTest, BrowserWithRegularAndPinnedTabs_ClosePolicyRegularTabs) {
  WebStateList* web_state_list = browser()->GetWebStateList();

  web::WebState* web_state0 =
      AppendWebState(InsertionPolicy::kPinned, WebStateOpener{});
  web::WebState* web_state1 =
      AppendWebState(InsertionPolicy::kPinned, WebStateOpener{web_state0, 0});
  web::WebState* web_state2 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{});
  web::WebState* web_state3 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{web_state1, 0});
  web::WebState* web_state4 =
      AppendWebState(InsertionPolicy::kRegular, WebStateOpener{web_state3, 0});

  ASSERT_EQ(web_state_list->count(), 5);
  ASSERT_EQ(web_state_list->GetWebStateAt(0), web_state0);
  ASSERT_EQ(web_state_list->GetWebStateAt(1), web_state1);
  ASSERT_EQ(web_state_list->GetWebStateAt(2), web_state2);
  ASSERT_EQ(web_state_list->GetWebStateAt(3), web_state3);
  ASSERT_EQ(web_state_list->GetWebStateAt(4), web_state4);

  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kRegularTabs);

  // Check that some tabs can be closed.
  EXPECT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that calling CloseTabs() closes only the regular tabs,
  // allow to undo the operation and leave the WebStateList with
  // only pinned tabs.
  EXPECT_EQ(tabs_closer.CloseTabs(), 3);
  EXPECT_TRUE(tabs_closer.CanUndoCloseTabs());
  EXPECT_EQ(web_state_list->count(), 2);
  EXPECT_EQ(web_state_list->pinned_tabs_count(), 2);

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as it has not been confirmed).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling UndoCloseTabs() correctly restores all the closed tabs,
  // in the correct order, and with the opener-opened relationship intact.
  EXPECT_EQ(tabs_closer.UndoCloseTabs(), 3);
  ASSERT_EQ(web_state_list->count(), 5);
  EXPECT_EQ(web_state_list->pinned_tabs_count(), 2);
  EXPECT_EQ(web_state_list->GetWebStateAt(0), web_state0);
  EXPECT_EQ(web_state_list->GetWebStateAt(1), web_state1);
  EXPECT_EQ(web_state_list->GetWebStateAt(2), web_state2);
  EXPECT_EQ(web_state_list->GetWebStateAt(3), web_state3);
  EXPECT_EQ(web_state_list->GetWebStateAt(4), web_state4);
  EXPECT_EQ(web_state_list->GetOpenerOfWebStateAt(1).opener, web_state0);
  EXPECT_EQ(web_state_list->GetOpenerOfWebStateAt(3).opener, web_state1);
  EXPECT_EQ(web_state_list->GetOpenerOfWebStateAt(4).opener, web_state3);

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as the operation has been cancelled).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling CloseTabs() and ConfirmDeletion() correctly closes the
  // tabs and prevents undo.
  ASSERT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_EQ(tabs_closer.CloseTabs(), 3);
  EXPECT_EQ(tabs_closer.ConfirmDeletion(), 3);
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that the TabRestoreService has now been informed of the
  // close operation which has been confirmed.
  EXPECT_EQ(restore_service()->entries().size(), 3u);
}

// Tests that TabsCloser reinstates the groups when undoing.
//
// Variants: ClosePolicy::kAllTabs
TEST_F(TabsCloserTest, GroupedTabs_ClosePolicyAllTabs) {
  WebStateList* web_state_list = browser()->GetWebStateList();
  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "a b | c [ 0 d e ] f [ 1 g h i ] j", browser()->GetBrowserState()));
  // Store the initial groups visual data to compare after Undo.
  const tab_groups::TabGroupVisualData visual_data_0 =
      builder.GetTabGroupForIdentifier('0')->visual_data();
  const tab_groups::TabGroupVisualData visual_data_1 =
      builder.GetTabGroupForIdentifier('1')->visual_data();
  // Store the initial WebStates to compare after Undo.
  std::vector<web::WebState*> initial_web_states;
  for (int i = 0; i < web_state_list->count(); ++i) {
    initial_web_states.push_back(web_state_list->GetWebStateAt(i));
  }

  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kAllTabs);

  // Check that some tabs can be closed.
  EXPECT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that calling CloseTabs() closes all the tabs registered, allows to
  // undo the operation, and leaves the WebStateList empty.
  EXPECT_EQ(tabs_closer.CloseTabs(), 10);
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_TRUE(tabs_closer.CanUndoCloseTabs());
  EXPECT_EQ("|", builder.GetWebStateListDescription());

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as it has not been confirmed).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling UndoCloseTabs() correctly restores all the closed tabs,
  // in the correct order, and in the correct groups (note that the identifiers
  // have been lost but the structure is the same).
  EXPECT_EQ(tabs_closer.UndoCloseTabs(), 10);
  EXPECT_EQ("_ _ | _ [ _ _ _ ] _ [ _ _ _ _ ] _",
            builder.GetWebStateListDescription());
  // Compare the group's visual data with the initial ones.
  const TabGroup* actual_group_0 = web_state_list->GetGroupOfWebStateAt(3);
  EXPECT_EQ(visual_data_0, actual_group_0->visual_data());
  const TabGroup* actual_group_1 = web_state_list->GetGroupOfWebStateAt(6);
  EXPECT_EQ(visual_data_1, actual_group_1->visual_data());
  // Compare the WebStates with the initial ones.
  for (int i = 0; i < web_state_list->count(); ++i) {
    EXPECT_EQ(initial_web_states[i], web_state_list->GetWebStateAt(i));
  }

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as the operation has been cancelled).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling CloseTabs() and ConfirmDeletion() correctly closes the
  // tabs and prevents undo.
  ASSERT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_EQ(tabs_closer.CloseTabs(), 10);
  EXPECT_EQ(tabs_closer.ConfirmDeletion(), 10);
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());
  EXPECT_EQ("|", builder.GetWebStateListDescription());

  // Check that the TabRestoreService has now been informed of the
  // close operation which has been confirmed.
  EXPECT_EQ(restore_service()->entries().size(), 10u);
}

// Tests that TabsCloser reinstates the groups when undoing.
//
// Variants: ClosePolicy::kRegularTabs
TEST_F(TabsCloserTest, GroupedTabs_ClosePolicyRegularTabs) {
  WebStateList* web_state_list = browser()->GetWebStateList();
  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "a b | c [ 0 d e ] f [ 1 g h i ] j", browser()->GetBrowserState()));
  // Store the initial groups visual data to compare after Undo.
  const tab_groups::TabGroupVisualData visual_data_0 =
      builder.GetTabGroupForIdentifier('0')->visual_data();
  const tab_groups::TabGroupVisualData visual_data_1 =
      builder.GetTabGroupForIdentifier('1')->visual_data();
  // Store the initial WebStates to compare after Undo.
  std::vector<web::WebState*> initial_web_states;
  for (int i = 0; i < web_state_list->count(); ++i) {
    initial_web_states.push_back(web_state_list->GetWebStateAt(i));
  }

  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kRegularTabs);

  // Check that some tabs can be closed.
  EXPECT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());

  // Check that calling CloseTabs() closes all the tabs registered, allows to
  // undo the operation, and leaves the WebStateList with only pinned tabs.
  EXPECT_EQ(tabs_closer.CloseTabs(), 8);
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_TRUE(tabs_closer.CanUndoCloseTabs());
  EXPECT_EQ("a b |", builder.GetWebStateListDescription());

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as it has not been confirmed).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling UndoCloseTabs() correctly restores all the closed tabs,
  // in the correct order, and in the correct groups (note that the identifiers
  // have been lost but the structure is the same).
  EXPECT_EQ(tabs_closer.UndoCloseTabs(), 8);
  EXPECT_EQ("a b | _ [ _ _ _ ] _ [ _ _ _ _ ] _",
            builder.GetWebStateListDescription());
  // Compare the group's visual data with the initial ones.
  const TabGroup* actual_group_0 = web_state_list->GetGroupOfWebStateAt(3);
  EXPECT_EQ(visual_data_0, actual_group_0->visual_data());
  const TabGroup* actual_group_1 = web_state_list->GetGroupOfWebStateAt(6);
  EXPECT_EQ(visual_data_1, actual_group_1->visual_data());
  // Compare the WebStates with the initial ones.
  for (int i = 0; i < web_state_list->count(); ++i) {
    EXPECT_EQ(initial_web_states[i], web_state_list->GetWebStateAt(i));
  }

  // Check that the TabRestoreService has not been informed of the close
  // operation yet (as the operation has been cancelled).
  EXPECT_EQ(restore_service()->entries().size(), 0u);

  // Check that calling CloseTabs() and ConfirmDeletion() correctly closes the
  // tabs and prevents undo.
  ASSERT_TRUE(tabs_closer.CanCloseTabs());
  EXPECT_EQ(tabs_closer.CloseTabs(), 8);
  EXPECT_EQ(tabs_closer.ConfirmDeletion(), 8);
  EXPECT_FALSE(tabs_closer.CanCloseTabs());
  EXPECT_FALSE(tabs_closer.CanUndoCloseTabs());
  EXPECT_EQ("a b |", builder.GetWebStateListDescription());

  // Check that the TabRestoreService has now been informed of the
  // close operation which has been confirmed.
  EXPECT_EQ(restore_service()->entries().size(), 8u);
}

// Check that TabsCloser returns that there it is not possible to undo a close
// operation while undo is in progress.
TEST_F(TabsCloserTest, UndoCloseTabs_Reentrancy) {
  WebStateList* web_state_list = browser()->GetWebStateList();
  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "a b | c d e", browser()->GetBrowserState()));

  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kAllTabs);

  EXPECT_EQ(tabs_closer.CloseTabs(), 5);

  ReentrancyObserver observer(tabs_closer);
  base::ScopedObservation<WebStateList, WebStateListObserver> scoped_observer(
      &observer);
  scoped_observer.Observe(web_state_list);

  tabs_closer.UndoCloseTabs();
  EXPECT_TRUE(observer.CheckThatUndoCloseTabsWasNotPossible());
}

// Checks that close all/undo is correctly updating the TabGroupSyncService,
// both when it hasn't been modified and when it has been modified.
TEST_F(TabsCloserTest, UndoCloseTabs_SavedTabs) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitWithFeatures(
      {kTabGroupsInGrid, kTabGroupsIPad, kModernTabStrip, kTabGroupSync}, {});

  WebStateList* web_state_list = browser()->GetWebStateList();
  WebStateListBuilderFromDescription builder(web_state_list);
  ASSERT_TRUE(builder.BuildWebStateListFromDescription(
      "| a [ 0 b ] c d [ 1 e ]", browser()->GetBrowserState()));

  // Add the two groups.
  tab_groups::FakeTabGroupSyncService* service = tab_group_service();
  tab_groups::TabGroupId first_local_id =
      web_state_list->GetGroupOfWebStateAt(1)->tab_group_id();
  web::WebStateID first_tab_id =
      web_state_list->GetWebStateAt(1)->GetUniqueIdentifier();
  base::Uuid first_group_id = base::Uuid::GenerateRandomV4();
  tab_groups::SavedTabGroupTab first_tab(
      GURL("http://first-tab.com"), u"first tab", first_group_id,
      std::make_optional(0), base::Uuid::GenerateRandomV4(),
      first_tab_id.identifier());
  tab_groups::SavedTabGroup first_saved_group(
      u"first title", tab_groups::TabGroupColorId::kBlue, {first_tab},
      std::make_optional(0), first_group_id, first_local_id);
  service->AddGroup(first_saved_group);

  tab_groups::TabGroupId second_local_id =
      web_state_list->GetGroupOfWebStateAt(4)->tab_group_id();
  base::Uuid second_group_id = base::Uuid::GenerateRandomV4();
  web::WebStateID second_tab_id =
      web_state_list->GetWebStateAt(4)->GetUniqueIdentifier();
  tab_groups::SavedTabGroupTab second_tab(
      GURL("http://second-tab.com"), u"second tab", second_group_id,
      std::make_optional(0), base::Uuid::GenerateRandomV4(),
      second_tab_id.identifier());
  tab_groups::SavedTabGroup second_saved_group(
      u"second title", tab_groups::TabGroupColorId::kBlue, {second_tab},
      std::make_optional(0), second_group_id, second_local_id);
  service->AddGroup(second_saved_group);

  TabsCloser tabs_closer(browser(), TabsCloser::ClosePolicy::kRegularTabs);

  // First check: no modification between Close All and Undo.

  // Close all.
  EXPECT_EQ(tabs_closer.CloseTabs(), 5);

  // Check that the two groups are still in the service, with no local group /
  // tab.
  EXPECT_TRUE(service->GetGroup(first_group_id));
  EXPECT_FALSE(service->GetGroup(first_group_id)->local_group_id().has_value());
  EXPECT_FALSE(service->GetGroup(first_group_id)
                   ->saved_tabs()[0]
                   .local_tab_id()
                   .has_value());
  EXPECT_TRUE(service->GetGroup(second_group_id));
  EXPECT_FALSE(
      service->GetGroup(second_group_id)->local_group_id().has_value());
  EXPECT_FALSE(service->GetGroup(second_group_id)
                   ->saved_tabs()[0]
                   .local_tab_id()
                   .has_value());

  // Undo.
  tabs_closer.UndoCloseTabs();

  // Check that the group are re-associated.
  EXPECT_EQ(2ul, web_state_list->GetGroups().size());
  EXPECT_EQ(5, web_state_list->count());
  EXPECT_TRUE(service->GetGroup(first_group_id));
  EXPECT_EQ(first_local_id,
            service->GetGroup(first_group_id)->local_group_id().value());
  EXPECT_TRUE(service->GetGroup(second_group_id));
  EXPECT_EQ(second_local_id,
            service->GetGroup(second_group_id)->local_group_id().value());

  // Second check: a group has been removed between Close All and Undo.

  // Close all.
  EXPECT_EQ(tabs_closer.CloseTabs(), 5);

  // Check that the two groups are still in the service, with no local group /
  // tab.
  EXPECT_TRUE(service->GetGroup(first_group_id));
  EXPECT_FALSE(service->GetGroup(first_group_id)->local_group_id().has_value());
  EXPECT_TRUE(service->GetGroup(second_group_id));
  EXPECT_FALSE(
      service->GetGroup(second_group_id)->local_group_id().has_value());

  // Remove a group from a sync update.
  service->RemoveGroup(first_group_id);

  // Undo.
  tabs_closer.UndoCloseTabs();

  // Check that the first group deleted (but not its tabs) and the second is
  // re-associated.
  EXPECT_EQ(1ul, web_state_list->GetGroups().size());
  EXPECT_EQ(5, web_state_list->count());
  EXPECT_FALSE(service->GetGroup(first_group_id));
  EXPECT_EQ(second_local_id,
            web_state_list->GetGroupOfWebStateAt(4)->tab_group_id());
  EXPECT_TRUE(service->GetGroup(second_group_id));
  EXPECT_EQ(second_local_id,
            service->GetGroup(second_group_id)->local_group_id().value());
}