chromium/ios/chrome/browser/sessions/model/session_restoration_service_impl_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/sessions/model/session_restoration_service_impl.h"

#import <map>
#import <set>

#import "base/barrier_closure.h"
#import "base/check_op.h"
#import "base/containers/span.h"
#import "base/files/file_enumerator.h"
#import "base/files/file_util.h"
#import "base/files/scoped_temp_dir.h"
#import "base/functional/bind.h"
#import "base/memory/raw_ptr.h"
#import "base/memory/weak_ptr.h"
#import "base/run_loop.h"
#import "base/scoped_multi_source_observation.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/time/time.h"
#import "ios/chrome/browser/sessions/model/proto/storage.pb.h"
#import "ios/chrome/browser/sessions/model/session_constants.h"
#import "ios/chrome/browser/sessions/model/session_internal_util.h"
#import "ios/chrome/browser/sessions/model/session_loading.h"
#import "ios/chrome/browser/sessions/model/session_restoration_service_tmpl.h"
#import "ios/chrome/browser/sessions/model/test_session_restoration_observer.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/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/chrome/browser/web/model/chrome_web_client.h"
#import "ios/web/common/user_agent.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/navigation/navigation_manager.h"
#import "ios/web/public/navigation/navigation_util.h"
#import "ios/web/public/navigation/referrer.h"
#import "ios/web/public/session/proto/navigation.pb.h"
#import "ios/web/public/session/proto/proto_util.h"
#import "ios/web/public/session/proto/storage.pb.h"
#import "ios/web/public/test/scoped_testing_web_client.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer.h"
#import "testing/platform_test.h"
#import "ui/base/page_transition_types.h"
#import "ui/base/window_open_disposition.h"
#import "url/gurl.h"

namespace {

// Set of FilePath.
using FilePathSet = std::set<base::FilePath>;

// Delay before saving the session to storage during test. This makes the
// test independent from the delay used in production (i.e. does not need
// to know how long to skip ahead with
constexpr base::TimeDelta kSaveDelay = base::Seconds(1);

// Identifier used for the Browser under test.
const char kIdentifier0[] = "browser0";
const char kIdentifier1[] = "browser1";

// List of URLs that are loaded in the session.
constexpr std::string_view kURLs[] = {
    "chrome://version",
    "chrome://flags",
    "chrome://credits",
};

// URL and title used to create an unrealized WebState.
const char kURL[] = "https://example.com";
const char16_t kTitle[] = u"Example Domain";

// Helper used to check whether a callback has been invoked and/or destroyed.
template <typename Ret, typename... Args>
struct Wrapper {
 private:
  using Callback = base::OnceCallback<Ret(Args...)>;

  struct Flag final {
    explicit Flag(Wrapper* owner, Callback callback)
        : owner_(owner), callback_(std::move(callback)) {}

    Ret Run(Args... args) {
      if (owner_) {
        owner_->callback_called_ = true;
        owner_ = nullptr;
      }

      return std::move(callback_).Run(std::move(args)...);
    }

    raw_ptr<Wrapper<Ret, Args...>> owner_;
    Callback callback_;
    base::WeakPtrFactory<Flag> weak_ptr_factory_{this};
  };

 public:
  Wrapper(Callback callback) {
    auto flag = std::make_unique<Flag>(this, std::move(callback));
    flag_ = flag->weak_ptr_factory_.GetWeakPtr();

    callback_ = base::BindOnce(&Flag::Run, std::move(flag));
  }

  ~Wrapper() {
    Flag* flag = flag_.get();
    if (flag) {
      flag->owner_ = nullptr;
    }
  }

  Callback callback() { return std::move(callback_); }

  bool callback_called() { return callback_called_; }

  bool callback_destroyed() { return !flag_.get(); }

 private:
  Callback callback_;
  bool callback_called_ = false;
  base::WeakPtr<Flag> flag_;
};

template <typename Ret, typename... Args>
auto CallbackWrapper(base::OnceCallback<Ret(Args...)> callback) {
  return Wrapper(std::move(callback));
}

// Scoped observer template.
template <typename Source, typename Observer>
class ScopedObserver : public Observer {
 public:
  template <typename... Args>
  ScopedObserver(Args&&... args) : Observer(std::forward<Args>(args)...) {}

  // Register self as observing `source`.
  void Observe(Source* source) { observation_.AddObservation(source); }

  // Unregister self from any observed sources.
  void Reset() { observation_.RemoveAllObservations(); }

 private:
  base::ScopedMultiSourceObservation<Source, Observer> observation_{this};
};

// A WebStateObserver that invokes a callback when DidFinishNavigation() happen.
class TestWebStateObserver : public web::WebStateObserver {
 public:
  TestWebStateObserver(const base::RepeatingClosure& closure)
      : closure_(closure) {}

  // web::WebStateObserver implementation.
  void DidFinishNavigation(web::WebState* web_state,
                           web::NavigationContext* context) override {
    closure_.Run();
  }

 private:
  base::RepeatingClosure closure_;
};

// Scoped variant of observers.
using ScopedTestSessionRestorationObserver =
    ScopedObserver<SessionRestorationService, TestSessionRestorationObserver>;
using ScopedTestWebStateObserver =
    ScopedObserver<web::WebState, TestWebStateObserver>;

// Class that can be used to detect files that are modified.
class FileModificationTracker {
 public:
  FileModificationTracker() = default;

  // Records all existing files below `root` and their timestamp. Used to
  // detect created or updated files later in `ModifiedFiles(...)`.
  void Start(const base::FilePath& path) { snapshot_ = EnumerateFiles(path); }

  // Reports all created or updated files in `path` since call to `Start(...)`.
  FilePathSet ModifiedFiles(const base::FilePath& path) const {
    FilePathSet result;
    for (const auto& [name, time] : EnumerateFiles(path)) {
      auto iterator = snapshot_.find(name);
      if (iterator == snapshot_.end() || iterator->second != time) {
        result.insert(name);
      }
    }
    return result;
  }

  // Reports all deleted files in `path` since call to `Start(...)`.
  FilePathSet DeletedFiles(const base::FilePath& path) const {
    FilePathSet result;
    PathToTimeMap state = EnumerateFiles(path);
    for (const auto& [name, _] : snapshot_) {
      if (!base::Contains(state, name)) {
        result.insert(name);
      }
    }
    return result;
  }

 private:
  using PathToTimeMap = std::map<base::FilePath, base::Time>;

  // Returns a mapping of files to their last modified time below `path`.
  PathToTimeMap EnumerateFiles(const base::FilePath& path) const {
    PathToTimeMap result;

    base::FileEnumerator e(path, true, base::FileEnumerator::FileType::FILES);
    for (base::FilePath name = e.Next(); !name.empty(); name = e.Next()) {
      // Workaround for the fact that base::FileEnumerator::FileInfo drops the
      // sub-second precision when using GetLastModifiedTime() even when the
      // data is available. See https://crbug.com/1491766 for details.
      base::File::Info info;
      info.FromStat(e.GetInfo().stat());

      result.insert(std::make_pair(name, info.last_modified));
    }

    return result;
  }

  PathToTimeMap snapshot_;
};

// Structure storing a WebState and whether the native session is supposed
// to be available. Used by ExpectedStorageFilesForWebStates.
struct WebStateReference {
  raw_ptr<const web::WebState> web_state = nullptr;
  bool is_native_session_available = false;
};

// Returns the storage file for `references` in `session_dir`.
FilePathSet ExpectedStorageFilesForWebStates(
    const base::FilePath& session_dir,
    bool expect_session_metadata_storage,
    const std::vector<WebStateReference>& references) {
  FilePathSet result;
  if (expect_session_metadata_storage) {
    result.insert(session_dir.Append(kSessionMetadataFilename));
  }

  for (const WebStateReference& reference : references) {
    const base::FilePath web_state_dir = ios::sessions::WebStateDirectory(
        session_dir, reference.web_state->GetUniqueIdentifier());

    result.insert(web_state_dir.Append(kWebStateStorageFilename));
    if (reference.is_native_session_available) {
      result.insert(web_state_dir.Append(kWebStateSessionFilename));
    }
  }
  return result;
}

// Returns the path of storage file to `browser` in `session_dir`.
FilePathSet ExpectedStorageFilesForBrowser(
    const base::FilePath& session_dir,
    Browser* browser,
    bool expect_session_metadata_storage) {
  std::vector<WebStateReference> references;
  WebStateList* web_state_list = browser->GetWebStateList();
  for (int index = 0; index < web_state_list->count(); ++index) {
    references.push_back(WebStateReference{
        .web_state = web_state_list->GetWebStateAt(index),
        .is_native_session_available = true,
    });
  }
  return ExpectedStorageFilesForWebStates(
      session_dir, expect_session_metadata_storage, references);
}

// Set union.
FilePathSet operator+(const FilePathSet& lhs, const FilePathSet& rhs) {
  FilePathSet result;
  result.insert(lhs.begin(), lhs.end());
  result.insert(rhs.begin(), rhs.end());
  return result;
}

// Moves all WebStates from `src_web_state_list` to `dst_web_state_list` as
// a batch operation. This respects the `active` flag, but drop any existing
// opener-opened relationship.
void MoveWebStateBetweenWebStateList(WebStateList* src_web_state_list,
                                     WebStateList* dst_web_state_list) {
  auto src_lock = src_web_state_list->StartBatchOperation();
  auto dst_lock = dst_web_state_list->StartBatchOperation();

  const int active_index = src_web_state_list->active_index();
  src_web_state_list->ActivateWebStateAt(WebStateList::kInvalidIndex);

  while (!src_web_state_list->empty()) {
    const int index = src_web_state_list->count() - 1;
    const bool active = index == active_index;
    std::unique_ptr<web::WebState> web_state =
        src_web_state_list->DetachWebStateAt(index);

    dst_web_state_list->InsertWebState(
        std::move(web_state),
        WebStateList::InsertionParams::AtIndex(0).Activate(active));
  }
}

// Map WebStateID to timestamp.
using WebStateIDToTime = std::map<web::WebStateID, base::Time>;

// Collects the timestamp of the last committed items for each WebState's in
// `web_state_list` and returns a mapping from their identifier to the value.
WebStateIDToTime CollectLastCommittedItemTimestampFromWebStateList(
    WebStateList* web_state_list) {
  WebStateIDToTime result;

  const int web_state_list_count = web_state_list->count();
  for (int index = 0; index < web_state_list_count; ++index) {
    web::WebState* const web_state = web_state_list->GetWebStateAt(index);
    const web::WebStateID web_state_id = web_state->GetUniqueIdentifier();

    web::NavigationManager* const manager = web_state->GetNavigationManager();
    web::NavigationItem* const item = manager->GetLastCommittedItem();
    const base::Time timestamp = item->GetTimestamp();
    DCHECK_NE(timestamp, base::Time());

    result.insert(std::make_pair(web_state_id, timestamp));
  }

  return result;
}

// Extracts the timestamp of the last committed item from `storage`.
base::Time GetLastCommittedTimestampFromStorage(
    web::proto::WebStateStorage storage) {
  const auto& navigation_storage = storage.navigation();
  const int index = navigation_storage.last_committed_item_index();
  if (index < 0 || navigation_storage.items_size() <= index) {
    // Invalid last committed item index, return null timestamp.
    return base::Time();
  }

  const auto& item_storage = navigation_storage.items(index);
  return web::TimeFromProto(item_storage.timestamp());
}

}  // namespace

// The fixture used to test SessionRestorationServiceImpl.
//
// It uses a TaskEnvironment mocking the time to allow to easily control when
// SessionRestorationServiceImpl's task to save data to storage will execute.
class SessionRestorationServiceImplTest : public PlatformTest {
 public:
  SessionRestorationServiceImplTest() {
    // Use the ChromeWebClient as the test tries to load chrome:// URLs.
    scoped_web_client_ = std::make_unique<web::ScopedTestingWebClient>(
        std::make_unique<ChromeWebClient>());

    // Configure a WebTaskEnvironment with mocked time to be able to
    // fast-forward time and skip the delay before saving the data.
    web_task_environment_ = std::make_unique<web::WebTaskEnvironment>(
        base::test::TaskEnvironment::TimeSource::MOCK_TIME);

    // Create a test ChromeBrowserState and an object to track the files
    // that are created by the session restoration service operations.
    browser_state_ = TestChromeBrowserState::Builder().Build();
    file_tracker_.Start(browser_state_->GetStatePath());

    // Create the service, force enabling features support.
    service_ = std::make_unique<SessionRestorationServiceImpl>(
        kSaveDelay, /*enable_pinned_web_states=*/true,
        /*enable_tab_groups=*/true, browser_state_->GetStatePath(),
        base::SequencedTaskRunner::GetCurrentDefault());
  }

  ~SessionRestorationServiceImplTest() override { service_->Shutdown(); }

  // Returns the ChromeBrowserState used for tests.
  ChromeBrowserState* browser_state() { return browser_state_.get(); }

  // Returns the service under test.
  SessionRestorationService* service() { return service_.get(); }

  // Returns the path to the storage for Browser with `identifier`.
  base::FilePath SessionPathFromIdentifier(std::string_view identifier) {
    return browser_state_->GetStatePath()
        .Append(kSessionRestorationDirname)
        .Append(identifier);
  }

  // Inserts WebStates into `browser` each one loading a new URL from `urls`
  // and wait until all the WebStates are done with the navigation.
  void InsertTabsWithUrls(Browser& browser,
                          base::span<const std::string_view> urls) {
    base::RunLoop run_loop;
    ScopedTestWebStateObserver web_state_observer(
        base::BarrierClosure(std::size(urls), run_loop.QuitClosure()));

    WebStateList* web_state_list = browser.GetWebStateList();
    for (std::string_view url : urls) {
      std::unique_ptr<web::WebState> web_state =
          web::WebState::Create(web::WebState::CreateParams(browser_state()));

      web_state_observer.Observe(web_state.get());

      // The view of the WebState needs to be created before the navigation
      // is really executed.
      std::ignore = web_state->GetView();
      web_state->GetNavigationManager()->LoadURLWithParams(
          web::NavigationManager::WebLoadParams(GURL(url)));

      web_state_list->InsertWebState(
          std::move(web_state),
          WebStateList::InsertionParams::Automatic().Activate());
    }

    // Wait for the navigation to commit.
    run_loop.Run();
  }

  // Wait until all task posted on the background sequence are complete.
  void WaitForBackgroundTaskComplete() {
    base::RunLoop run_loop;
    service_->InvokeClosureWhenBackgroundProcessingDone(run_loop.QuitClosure());
    run_loop.Run();
  }

  // Wait until the save delay expired and then for all background task
  // to complete.
  void WaitForSessionSaveComplete() {
    // Fast forward the time to allow any timer to expire (and thus the
    // delayed save to be scheduled).
    web_task_environment_->FastForwardBy(kSaveDelay);

    WaitForBackgroundTaskComplete();
  }

  // Take a snapshot of the existing files.
  void SnapshotFiles() { file_tracker_.Start(browser_state_->GetStatePath()); }

  // Returns the list of modified files.
  FilePathSet ModifiedFiles() const {
    return file_tracker_.ModifiedFiles(browser_state_->GetStatePath());
  }

  // Returns the list of deleted files.
  FilePathSet DeletedFiles() const {
    return file_tracker_.DeletedFiles(browser_state_->GetStatePath());
  }

 private:
  FileModificationTracker file_tracker_;

  std::unique_ptr<web::ScopedTestingWebClient> scoped_web_client_;
  std::unique_ptr<web::WebTaskEnvironment> web_task_environment_;

  std::unique_ptr<TestChromeBrowserState> browser_state_;
  std::unique_ptr<SessionRestorationServiceImpl> service_;
};

// Tests that adding and removing observer works correctly.
TEST_F(SessionRestorationServiceImplTest, ObserverRegistration) {
  ScopedTestSessionRestorationObserver observer;
  ASSERT_FALSE(observer.IsInObserverList());

  // Check that registering/unregistering the observer works.
  observer.Observe(service());
  EXPECT_TRUE(observer.IsInObserverList());

  observer.Reset();
  EXPECT_FALSE(observer.IsInObserverList());
}

// Tests that SetSessionID does not load the session.
TEST_F(SessionRestorationServiceImplTest, SetSessionID) {
  ScopedTestSessionRestorationObserver observer;
  observer.Observe(service());

  // Check that calling SetSessionID() does not load the session.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);
  EXPECT_FALSE(observer.restore_started());

  // Check that calling Disconnect() when there are no changes on the
  // Browser does not cause the session to be saved.
  service()->Disconnect(&browser);
  WaitForSessionSaveComplete();

  // Check that no session file was written to disk.
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});
}

// Tests that LoadSession correctly loads the session from disk.
TEST_F(SessionRestorationServiceImplTest, LoadSession) {
  ScopedTestSessionRestorationObserver observer;
  observer.Observe(service());

  // Check that when a Browser is modified, the changes are reflected to the
  // storage after a delay.
  {
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);
    EXPECT_FALSE(observer.restore_started());

    // Insert a few WebState in the Browser's WebStateList.
    InsertTabsWithUrls(browser, base::make_span(kURLs));

    // Check that the session was written to disk.
    WaitForSessionSaveComplete();
    EXPECT_EQ(ModifiedFiles(),
              ExpectedStorageFilesForBrowser(
                  SessionPathFromIdentifier(kIdentifier0), &browser,
                  /*expect_session_metadata_storage=*/true));

    // Disconnect the Browser before destroying it. The service should no
    // longer track it and any modification should not be reflected.
    service()->Disconnect(&browser);

    // Check that closing the all the tabs after disconnecting the Browser
    // does not delete the sesion.
    SnapshotFiles();
    CloseAllWebStates(*browser.GetWebStateList(), WebStateList::CLOSE_NO_FLAGS);

    WaitForSessionSaveComplete();
    EXPECT_EQ(ModifiedFiles(), FilePathSet{});
    EXPECT_EQ(DeletedFiles(), FilePathSet{});
  }

  // Check that the session can be reloaded and that it contains the same
  // state as when it was saved.
  {
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);
    EXPECT_FALSE(observer.restore_started());

    // Perform session restore, and check that the expected WebState have
    // been recreated with the correct navigation history.
    service()->LoadSession(&browser);

    EXPECT_TRUE(observer.restore_started());
    EXPECT_EQ(observer.restored_web_states_count(),
              static_cast<int>(std::size(kURLs)));

    WebStateList* web_state_list = browser.GetWebStateList();
    EXPECT_EQ(web_state_list->count(), static_cast<int>(std::size(kURLs)));
    EXPECT_EQ(web_state_list->active_index(), web_state_list->count() - 1);
    for (int index = 0; index < web_state_list->count(); ++index) {
      web::WebState* web_state = web_state_list->GetWebStateAt(index);
      EXPECT_EQ(web_state->GetLastCommittedURL(), GURL(kURLs[index]));
    }

    // Disconnect the Browser before destroying it.
    service()->Disconnect(&browser);
  }
}

// Tests that LoadSession succeed even if the session is empty.
TEST_F(SessionRestorationServiceImplTest, LoadSession_EmptySession) {
  ScopedTestSessionRestorationObserver observer;
  observer.Observe(service());

  // Write an empty session.
  ios::proto::WebStateListStorage storage;
  storage.set_active_index(-1);

  const base::FilePath metadata_path =
      SessionPathFromIdentifier(kIdentifier0).Append(kSessionMetadataFilename);
  EXPECT_TRUE(ios::sessions::WriteProto(metadata_path, storage));

  // Check that the session can be loaded even if non-existent and that the
  // Browser is unmodified (but the observers notified).
  {
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);
    EXPECT_FALSE(observer.restore_started());

    // Check that loading the sessions succeed.
    service()->LoadSession(&browser);

    EXPECT_TRUE(observer.restore_started());
    EXPECT_EQ(observer.restored_web_states_count(), 0);

    // Disconnect the Browser before destroying it.
    service()->Disconnect(&browser);
  }
}

// Tests that LoadSession succeed even if the session does not exist.
TEST_F(SessionRestorationServiceImplTest, LoadSession_MissingSession) {
  ScopedTestSessionRestorationObserver observer;
  observer.Observe(service());

  // Check that the session can be loaded even if non-existent and that the
  // Browser is unmodified (but the observers notified).
  {
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);
    EXPECT_FALSE(observer.restore_started());

    // Check that loading the sessions succeed, even if there is no session.
    service()->LoadSession(&browser);

    EXPECT_TRUE(observer.restore_started());
    EXPECT_EQ(observer.restored_web_states_count(), 0);

    // Disconnect the Browser before destroying it.
    service()->Disconnect(&browser);
  }
}

// Tests that the service only saves the session of modified Browser.
TEST_F(SessionRestorationServiceImplTest, SaveSessionOfModifiedBrowser) {
  // Register multiple Browser and modify one of them. Check that
  // only data for the modified Browser is written to disk.
  TestBrowser browser0 = TestBrowser(browser_state());
  TestBrowser browser1 = TestBrowser(browser_state());
  service()->SetSessionID(&browser0, kIdentifier0);
  service()->SetSessionID(&browser1, kIdentifier1);

  // Insert a few WebState in browser1's WebStateList.
  InsertTabsWithUrls(browser1, base::make_span(kURLs));

  // Check that only browser1's session was written to disk.
  WaitForSessionSaveComplete();
  EXPECT_EQ(ModifiedFiles(),
            ExpectedStorageFilesForBrowser(
                SessionPathFromIdentifier(kIdentifier1), &browser1,
                /*expect_session_metadata_storage=*/true));

  // Disconnect the Browser before destroying it.
  service()->Disconnect(&browser1);
  service()->Disconnect(&browser0);
}

// Tests that the service only save content that has changed.
TEST_F(SessionRestorationServiceImplTest, SaveSessionChangesOnlyRequiredFiles) {
  // Create a Browser and add a few WebStates to it.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);
  InsertTabsWithUrls(browser, base::make_span(kURLs));

  // Check that the session was written to disk.
  WaitForSessionSaveComplete();
  EXPECT_EQ(ModifiedFiles(),
            ExpectedStorageFilesForBrowser(
                SessionPathFromIdentifier(kIdentifier0), &browser,
                /*expect_session_metadata_storage=*/true));

  // Record the list of existing files and their timestamp.
  SnapshotFiles();

  // Change the active WebState, and mark the new WebState as visited. This
  // should result in saving the session metadata (due to the change in the
  // WebStateList) and of one WebState (as its last active timestamp changed).
  ASSERT_NE(browser.GetWebStateList()->active_index(), 0);
  browser.GetWebStateList()->ActivateWebStateAt(0);
  browser.GetWebStateList()->GetActiveWebState()->WasShown();

  // Check that session metadata storage file and the active WebState storage
  // files are eventually saved.
  WaitForSessionSaveComplete();
  EXPECT_EQ(ModifiedFiles(),
            ExpectedStorageFilesForWebStates(
                SessionPathFromIdentifier(kIdentifier0),
                /*expect_session_metadata_storage=*/true,
                {WebStateReference{
                    .web_state = browser.GetWebStateList()->GetActiveWebState(),
                    .is_native_session_available = true,
                }}));

  // Disconnect the Browser before destroying it.
  service()->Disconnect(&browser);
}

// Tests that the service correctly support moving "unrealized" tabs between
// Browsers and that this results in a copy of the moved WebState's storage.
TEST_F(SessionRestorationServiceImplTest, AdoptUnrealizedWebStateOnMove) {
  // In order to have unrealized WebState in the Browser, create a Browser,
  // add some WebState, wait for the session to be serialized. The session
  // can then be loaed to get unrealized WebStates.
  {
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);

    // Insert a few WebState in the Browser's WebStateList.
    InsertTabsWithUrls(browser, base::make_span(kURLs));

    // Check that the session was written to disk.
    WaitForSessionSaveComplete();
    EXPECT_TRUE(base::DirectoryExists(SessionPathFromIdentifier(kIdentifier0)));

    // Disconnect the Browser before destroying it.
    service()->Disconnect(&browser);

    // Check that closing the all the tabs after disconnecting the Browser
    // does not delete the sesion.
    WaitForSessionSaveComplete();
    EXPECT_EQ(ModifiedFiles(),
              ExpectedStorageFilesForBrowser(
                  SessionPathFromIdentifier(kIdentifier0), &browser,
                  /*expect_session_metadata_storage=*/true));
  }

  // Load the session created before, and then move the tabs from the first
  // browser to the second one. Check that the session files have been copied.
  TestBrowser browser0 = TestBrowser(browser_state());
  TestBrowser browser1 = TestBrowser(browser_state());
  service()->SetSessionID(&browser0, kIdentifier0);
  service()->SetSessionID(&browser1, kIdentifier1);

  // Load the session in `browser0` and check that the expected number of tabs
  // are present and that the inactive tabs are not realized.
  service()->LoadSession(&browser0);
  service()->LoadSession(&browser1);

  // Record the list of existing files and their timestamp.
  SnapshotFiles();

  WebStateList* list0 = browser0.GetWebStateList();
  WebStateList* list1 = browser1.GetWebStateList();
  ASSERT_EQ(list0->count(), static_cast<int>(std::size(kURLs)));
  ASSERT_EQ(list1->count(), 0);

  // Check that the WebState are not realized.
  for (int index = 0; index < list0->count(); ++index) {
    web::WebState* web_state = list0->GetWebStateAt(index);
    EXPECT_FALSE(web_state->IsRealized());
  }

  // Move all tabs from browser0 to browser1 and check that this results in
  // the copy of the WebState's storage from browser0 to browser1 session
  // directory.
  //
  // Start by moving the inactive tabs, then move all the active tabs. The
  // move is done in reverse order to simplify the iteration (this allows
  // skipping the active_index during the iteration without going out of
  // range).
  const int old_count = list0->count();
  const int old_active_index = list0->active_index();
  ASSERT_EQ(list1->active_index(), WebStateList::kInvalidIndex);
  for (int index = 0; index < old_count; ++index) {
    const int reverse_index = old_count - 1 - index;
    if (reverse_index == old_active_index) {
      continue;
    }

    list1->InsertWebState(list0->DetachWebStateAt(reverse_index),
                          WebStateList::InsertionParams::AtIndex(0));
    ASSERT_EQ(list1->active_index(), WebStateList::kInvalidIndex);
  }

  ASSERT_EQ(list0->count(), 1);
  std::unique_ptr<web::WebState> web_state = list0->DetachWebStateAt(0);
  list1->InsertWebState(
      std::move(web_state),
      WebStateList::InsertionParams::AtIndex(old_active_index).Activate());

  ASSERT_EQ(list0->count(), 0);
  ASSERT_EQ(list1->count(), static_cast<int>(std::size(kURLs)));

  // Check that no files were deleted, the metadata for both session updated
  // and the WebState's storage copied from one Browser storage to the other.
  WaitForSessionSaveComplete();

  FilePathSet expected_browser0 = ExpectedStorageFilesForWebStates(
      SessionPathFromIdentifier(kIdentifier0),
      /*expect_session_metadata_storage=*/true, {});
  FilePathSet expected_browser1 = ExpectedStorageFilesForBrowser(
      SessionPathFromIdentifier(kIdentifier1), &browser1,
      /*expect_session_metadata_storage=*/true);

  EXPECT_EQ(ModifiedFiles(), expected_browser0 + expected_browser1);

  // Disconnect the Browser before destroying them.
  service()->Disconnect(&browser1);
  service()->Disconnect(&browser0);
}

// Tests that the service save pending changes on disconnect.
TEST_F(SessionRestorationServiceImplTest, SavePendingChangesOnDisconnect) {
  // Create a Browser and add a few WebStates to it.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);
  InsertTabsWithUrls(browser, base::make_span(kURLs));

  // Inserting the tabs may take more time than the save delay. Always
  // wait for the state to be saved so that the test is deterministic.
  WaitForSessionSaveComplete();
  EXPECT_EQ(ModifiedFiles(),
            ExpectedStorageFilesForBrowser(
                SessionPathFromIdentifier(kIdentifier0), &browser,
                /*expect_session_metadata_storage=*/true));

  // Record the list of existing files and their timestamp.
  SnapshotFiles();

  // Modify the Browser which will schedule a session after a delay.
  {
    WebStateList* web_state_list = browser.GetWebStateList();
    const int active_index = web_state_list->active_index();
    ASSERT_NE(active_index, 0);

    web_state_list->MoveWebStateAt(active_index, 0);
  }

  // Record the time and check that no file have been saved yet.
  const base::Time disconnect_time = base::Time::Now();
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});

  // Disconnect the Browser. This should save the session immediately.
  service()->Disconnect(&browser);

  // Not using `WaitForSessionSaveComplete()` because we explicitly do
  // not want to wait for the kSaveDelay timeout.
  WaitForBackgroundTaskComplete();

  // Check that even though the save delay has not expired, the data still
  // has been written to disk (because it was scheduled when the Browser
  // was disconnected as it contained pending changes).
  EXPECT_LT(base::Time::Now() - disconnect_time, kSaveDelay);
  EXPECT_EQ(ModifiedFiles(), ExpectedStorageFilesForWebStates(
                                 SessionPathFromIdentifier(kIdentifier0),
                                 /*expect_session_metadata_storage=*/true, {}));
}

// Tests that the service delete obsolete files when loading the session.
TEST_F(SessionRestorationServiceImplTest, DeleteObsoleteFilesOnLoadSession) {
  // Used to store the list of files that correspond to the session used
  // for the closed WebState.
  FilePathSet expected_deleted_files;

  {
    // Create a Browser and add a few WebStates to it.
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);
    InsertTabsWithUrls(browser, base::make_span(kURLs));

    // Inserting the tabs may take more time than the save delay. Always
    // wait for the state to be saved so that the test is deterministic.
    WaitForSessionSaveComplete();
    EXPECT_EQ(ModifiedFiles(),
              ExpectedStorageFilesForBrowser(
                  SessionPathFromIdentifier(kIdentifier0), &browser,
                  /*expect_session_metadata_storage=*/true));

    // Record the list of existing files and their timestamp.
    SnapshotFiles();

    // Detach one WebState from the Browser (not the active one).
    ASSERT_NE(browser.GetWebStateList()->active_index(), 0);
    std::unique_ptr<web::WebState> detached_web_state =
        browser.GetWebStateList()->DetachWebStateAt(0);

    // Wait for the sesssion to be saved, and check that no file were deleted,
    // but that the session metadata was updated.
    WaitForSessionSaveComplete();
    EXPECT_EQ(DeletedFiles(), FilePathSet{});
    EXPECT_EQ(ModifiedFiles(),
              ExpectedStorageFilesForWebStates(
                  SessionPathFromIdentifier(kIdentifier0),
                  /*expect_session_metadata_storage=*/true, {}));

    // Record the files used to represent the state of the detached WebState.
    // Those files will be deleted when the session is loaded.
    expected_deleted_files = ExpectedStorageFilesForWebStates(
        SessionPathFromIdentifier(kIdentifier0),
        /*expect_session_metadata_storage=*/false,
        {WebStateReference{.web_state = detached_web_state.get(),
                           .is_native_session_available = true}});

    // Disconnect the Browser before destroying it.
    service()->Disconnect(&browser);
  }

  // Record the list of existing files and their timestamp.
  SnapshotFiles();

  // Create a Browser and load the session in it.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);
  service()->LoadSession(&browser);

  // Check that the expected content was loaded and no files deleted yet (the
  // deletion is scheduled on the background sequence).
  EXPECT_EQ(browser.GetWebStateList()->count(),
            static_cast<int>(std::size(kURLs)) - 1);
  EXPECT_EQ(DeletedFiles(), FilePathSet{});

  // Wait for background processing to complete and check that the obsolete
  // session files have been deleted.
  WaitForSessionSaveComplete();
  EXPECT_EQ(DeletedFiles(), expected_deleted_files);

  // Disconnect the Browser before destroying it.
  service()->Disconnect(&browser);
}

// Tests that data is deleted when a WebState is closed while the Browser is
// still connected.
TEST_F(SessionRestorationServiceImplTest, DeleteDataOnClose) {
  // Insert a few WebState in a Browser, wait for the changes to be saved,
  // then destroy the Browser.
  {
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);

    InsertTabsWithUrls(browser, base::make_span(kURLs));
    WaitForSessionSaveComplete();

    service()->Disconnect(&browser);
  }

  // Create a new Browser and load the session.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);
  service()->LoadSession(&browser);

  WaitForSessionSaveComplete();
  SnapshotFiles();

  const FilePathSet expected_deleted_files = ExpectedStorageFilesForBrowser(
      SessionPathFromIdentifier(kIdentifier0), &browser,
      /*expect_session_metadata_storage=*/false);

  const FilePathSet expected_modified_files = ExpectedStorageFilesForWebStates(
      SessionPathFromIdentifier(kIdentifier0),
      /*expect_session_metadata_storage=*/true, {});

  // Close all WebStates, check that the data is deleted.
  CloseAllWebStates(*browser.GetWebStateList(), WebStateList::CLOSE_NO_FLAGS);

  WaitForSessionSaveComplete();

  EXPECT_EQ(DeletedFiles(), expected_deleted_files);
  EXPECT_EQ(ModifiedFiles(), expected_modified_files);

  service()->Disconnect(&browser);
}

// Tests that data is deleted when a WebState is closed while the Browser is
// still connected, after being moved from between Browsers without leaving
// time for the session to be saved.
TEST_F(SessionRestorationServiceImplTest, DeleteDataOnClose_AfterMove) {
  // Insert a few WebState in a Browser, wait for the changes to be saved,
  // then destroy the Browser.
  {
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);

    InsertTabsWithUrls(browser, base::make_span(kURLs));
    WaitForSessionSaveComplete();

    service()->Disconnect(&browser);
  }

  // Create two Browsers, load the data in one of the Browser.
  TestBrowser browser0 = TestBrowser(browser_state());
  TestBrowser browser1 = TestBrowser(browser_state());
  service()->SetSessionID(&browser0, kIdentifier0);
  service()->SetSessionID(&browser1, kIdentifier1);
  service()->LoadSession(&browser0);

  WaitForSessionSaveComplete();
  SnapshotFiles();

  const FilePathSet expected_deleted_files = ExpectedStorageFilesForBrowser(
      SessionPathFromIdentifier(kIdentifier0), &browser0,
      /*expect_session_metadata_storage=*/false);

  const FilePathSet expected_modified_files =
      ExpectedStorageFilesForWebStates(SessionPathFromIdentifier(kIdentifier0),
                                       /*expect_session_metadata_storage=*/true,
                                       {}) +
      ExpectedStorageFilesForWebStates(SessionPathFromIdentifier(kIdentifier1),
                                       /*expect_session_metadata_storage=*/true,
                                       {});

  // Move all WebState between Browser, then close them. Confirm that the
  // data have been deleted from the original Browser.
  MoveWebStateBetweenWebStateList(browser0.GetWebStateList(),
                                  browser1.GetWebStateList());

  CloseAllWebStates(*browser1.GetWebStateList(), WebStateList::CLOSE_NO_FLAGS);

  WaitForSessionSaveComplete();

  EXPECT_EQ(DeletedFiles(), expected_deleted_files);
  EXPECT_EQ(ModifiedFiles(), expected_modified_files);

  service()->Disconnect(&browser1);
  service()->Disconnect(&browser0);
}

// Tests that histograms are correctly recorded.
TEST_F(SessionRestorationServiceImplTest, RecordHistograms) {
  {
    // Create a Browser and add a few WebStates to it and wait for all
    // pending scheduled tasks to complete.
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);
    InsertTabsWithUrls(browser, base::make_span(kURLs));
    WaitForSessionSaveComplete();

    // Check that session is saved and histogram is recorded when making
    // some changes to the Browser's WebStateList (changing the active
    // index).
    base::HistogramTester histogram_tester;
    ASSERT_NE(browser.GetWebStateList()->active_index(), 0);
    browser.GetWebStateList()->ActivateWebStateAt(0);
    WaitForSessionSaveComplete();

    // Check that the time spent to record the session was logged.
    histogram_tester.ExpectTotalCount(kSessionHistogramSavingTime, 1);

    // Disconnect the Browser before destroying it.
    service()->Disconnect(&browser);
  }

  // Create a Browser.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);

  // Load the session and check that the time spent loading was logged.
  base::HistogramTester histogram_tester;
  service()->LoadSession(&browser);

  // Check that the expected content was loaded.
  EXPECT_EQ(browser.GetWebStateList()->count(),
            static_cast<int>(std::size(kURLs)));
  histogram_tester.ExpectTotalCount(kSessionHistogramLoadingTime, 1);

  // Disconnect the Browser before destroying it.
  service()->Disconnect(&browser);
}

// Tests that creating an unrealized WebState succeed and that the data
// is correctly saved to the disk.
TEST_F(SessionRestorationServiceImplTest, CreateUnrealizedWebState) {
  // Create a Browser.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);

  // Create an unrealized WebState.
  std::unique_ptr<web::WebState> web_state =
      service()->CreateUnrealizedWebState(
          &browser,
          web::CreateWebStateStorage(
              web::NavigationManager::WebLoadParams(GURL(kURL)), kTitle, false,
              web::UserAgentType::MOBILE, base::Time::Now()));
  ASSERT_TRUE(web_state);

  // Record the list of expected files while the pointer to the newly created
  // WebState is still valid.
  const FilePathSet expected_files =
      ExpectedStorageFilesForWebStates(SessionPathFromIdentifier(kIdentifier0),
                                       /*expect_session_metadata_storage=*/true,
                                       {WebStateReference{
                                           .web_state = web_state.get(),
                                           .is_native_session_available = false,
                                       }});

  // Insert the WebState into the Browser's WebStateList and then wait for
  // the session to be saved to storage.
  browser.GetWebStateList()->InsertWebState(
      std::move(web_state),
      WebStateList::InsertionParams::Automatic().Activate());
  WaitForSessionSaveComplete();

  // Check that the data for the WebState has been saved to disk.
  EXPECT_EQ(ModifiedFiles(), expected_files);

  // Disconnect the Browser before destroying it.
  service()->Disconnect(&browser);
}

// Tests that calling SaveSessions() can be done at any point in time.
TEST_F(SessionRestorationServiceImplTest, SaveSessionsCallableAtAnyTime) {
  // Check that calling SaveSessions() when no Browser is observed is a no-op.
  service()->SaveSessions();

  WaitForBackgroundTaskComplete();
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});

  // Check that calling SaveSessions() when Browser are registered with no
  // changes is a no-op.
  TestBrowser browser0 = TestBrowser(browser_state());
  TestBrowser browser1 = TestBrowser(browser_state());
  service()->SetSessionID(&browser0, kIdentifier0);
  service()->SetSessionID(&browser1, kIdentifier1);

  service()->SaveSessions();

  WaitForBackgroundTaskComplete();
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});

  // Insert a few WebStage in one of the Browser and wait for the changes
  // to automatically be saved (this is because loading the pages will
  // take time and may cause automatically saving the session).
  {
    InsertTabsWithUrls(browser0, base::make_span(kURLs));
    WaitForSessionSaveComplete();

    EXPECT_EQ(ModifiedFiles(),
              ExpectedStorageFilesForBrowser(
                  SessionPathFromIdentifier(kIdentifier0), &browser0,
                  /*expect_session_metadata_storage=*/true));

    SnapshotFiles();
  }

  // Check that making a modification and then calling SaveSessions() will
  // result in a save immediately, even without waiting for the save delay.
  ASSERT_NE(browser0.GetWebStateList()->active_index(), 0);
  browser0.GetWebStateList()->ActivateWebStateAt(0);

  WaitForBackgroundTaskComplete();
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});

  service()->SaveSessions();
  WaitForBackgroundTaskComplete();
  EXPECT_EQ(ModifiedFiles(), ExpectedStorageFilesForWebStates(
                                 SessionPathFromIdentifier(kIdentifier0),
                                 /*expect_session_metadata_storage=*/true, {}));

  SnapshotFiles();

  // Check that calling SaveSessions() when all Browser have been disconnected
  // is a no-op.
  service()->Disconnect(&browser0);
  service()->Disconnect(&browser1);

  service()->SaveSessions();

  WaitForBackgroundTaskComplete();
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});
}

// Tests that calling ScheduleSaveSessions() is a no-op.
TEST_F(SessionRestorationServiceImplTest, ScheduleSaveSessions) {
  // Check that calling ScheduleSaveSessions() when no Browser is observed
  // is a no-op.
  service()->ScheduleSaveSessions();

  WaitForBackgroundTaskComplete();
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});

  // Check that calling ScheduleSaveSessions() when Browser are registered
  // with no changes is a no-op.
  TestBrowser browser0 = TestBrowser(browser_state());
  TestBrowser browser1 = TestBrowser(browser_state());
  service()->SetSessionID(&browser0, kIdentifier0);
  service()->SetSessionID(&browser1, kIdentifier1);

  service()->ScheduleSaveSessions();

  WaitForBackgroundTaskComplete();
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});

  // Insert a few WebStage in one of the Browser and wait for the changes
  // to automatically be saved (this is because loading the pages will
  // take time and may cause automatically saving the session).
  {
    InsertTabsWithUrls(browser0, base::make_span(kURLs));
    WaitForSessionSaveComplete();

    EXPECT_EQ(ModifiedFiles(),
              ExpectedStorageFilesForBrowser(
                  SessionPathFromIdentifier(kIdentifier0), &browser0,
                  /*expect_session_metadata_storage=*/true));

    SnapshotFiles();
  }

  // Check that making a modification and then calling ScheduleSaveSessions()
  // is also a no-op, and that the save will only happen after the save delay
  // has expired.
  ASSERT_NE(browser0.GetWebStateList()->active_index(), 0);
  browser0.GetWebStateList()->ActivateWebStateAt(0);

  WaitForBackgroundTaskComplete();
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});

  service()->ScheduleSaveSessions();
  WaitForBackgroundTaskComplete();
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});

  // Check that the session are saved after waiting to the save delay.
  WaitForSessionSaveComplete();
  EXPECT_EQ(ModifiedFiles(), ExpectedStorageFilesForWebStates(
                                 SessionPathFromIdentifier(kIdentifier0),
                                 /*expect_session_metadata_storage=*/true, {}));

  SnapshotFiles();

  // Check that calling SaveSessions() when all Browser have been disconnected
  // is a no-op.
  service()->Disconnect(&browser0);
  service()->Disconnect(&browser1);

  service()->ScheduleSaveSessions();

  WaitForBackgroundTaskComplete();
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});
}

// Tests that calling DeleteDataForDiscardedSessions() deletes data for
// discarded sessions and accept inexistant sessions identifiers.
TEST_F(SessionRestorationServiceImplTest, DeleteDataForDiscardedSessions) {
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);

  // Insert a few WebStage in one of the Browser and wait for the changes
  // to automatically be saved (this is because loading the pages will
  // take time and may cause automatically saving the session).
  InsertTabsWithUrls(browser, base::make_span(kURLs));
  WaitForSessionSaveComplete();

  // Record the file that make the storage for `browser`.
  const FilePathSet browser_storage = ExpectedStorageFilesForBrowser(
      SessionPathFromIdentifier(kIdentifier0), &browser,
      /*expect_session_metadata_storage=*/true);

  EXPECT_EQ(ModifiedFiles(), browser_storage);

  service()->Disconnect(&browser);
  WaitForSessionSaveComplete();

  SnapshotFiles();

  // Ask for deletion of session for the disconnected Browser's identifier
  // and for a non-existent identifier.
  base::RunLoop run_loop;
  service()->DeleteDataForDiscardedSessions({kIdentifier0, kIdentifier1},
                                            run_loop.QuitClosure());
  run_loop.Run();

  // Verify that the files for Browser have been deleted.
  EXPECT_EQ(DeletedFiles(), browser_storage);
}

// Tests that PurgeUnassociatedData() can be called at any point as the
// SessionRestorationServiceImpl version is a no-op.
TEST_F(SessionRestorationServiceImplTest, PurgeUnassociatedData) {
  // Test that the method can be called before any Browser has been
  // registered.
  {
    base::RunLoop run_loop;
    service()->PurgeUnassociatedData(run_loop.QuitClosure());
    run_loop.Run();
  }

  // Test that the method can be called after a Browser has been
  // registered.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);
  {
    base::RunLoop run_loop;
    service()->PurgeUnassociatedData(run_loop.QuitClosure());
    run_loop.Run();
  }

  service()->Disconnect(&browser);
}

// Tests that LoadWebStateStorage(...) loads the data from disk.
TEST_F(SessionRestorationServiceImplTest, LoadWebStateData) {
  // Insert a few WebState in a Browser, wait for the changes to be saved,
  // then destroy the Browser.
  {
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);

    InsertTabsWithUrls(browser, base::make_span(kURLs));
    WaitForSessionSaveComplete();

    service()->Disconnect(&browser);
  }

  // Create a new Browser and load the session. There should be at least
  // one unrealized WebState.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);
  service()->LoadSession(&browser);

  ASSERT_FALSE(browser.GetWebStateList()->empty());
  const int index = browser.GetWebStateList()->count() - 1;
  web::WebState* web_state = browser.GetWebStateList()->GetWebStateAt(index);
  ASSERT_FALSE(web_state->IsRealized());

  web::proto::WebStateStorage storage;
  auto callback = base::BindOnce(
      [](web::proto::WebStateStorage* out_storage,
         web::proto::WebStateStorage loaded_storage) {
        *out_storage = std::move(loaded_storage);
      },
      &storage);

  // Check that calling LoadWebStateStorage(...) load the data.
  base::RunLoop run_loop;
  service()->LoadWebStateStorage(
      &browser, web_state, std::move(callback).Then(run_loop.QuitClosure()));
  run_loop.Run();

  ASSERT_TRUE(storage.has_navigation());
  ASSERT_GE(storage.navigation().items_size(), 1);
  EXPECT_EQ(GURL(storage.navigation().items(0).url()), GURL(kURLs[index]));

  service()->Disconnect(&browser);
}

// Tests that LoadWebStateStorage(...) does not call the callback if the
// browser is not registered.
TEST_F(SessionRestorationServiceImplTest, LoadWebStateData_Disconnected) {
  // Insert a few WebState in a Browser, wait for the changes to be saved,
  // then destroy the Browser.
  {
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);

    InsertTabsWithUrls(browser, base::make_span(kURLs));
    WaitForSessionSaveComplete();

    service()->Disconnect(&browser);
  }

  // Create a new Browser and load the session. There should be at least
  // one unrealized WebState.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);
  service()->LoadSession(&browser);

  ASSERT_FALSE(browser.GetWebStateList()->empty());
  const int index = browser.GetWebStateList()->count() - 1;
  web::WebState* web_state = browser.GetWebStateList()->GetWebStateAt(index);
  ASSERT_FALSE(web_state->IsRealized());

  // Disconnect the Browser and check that calling LoadWebStateStorage(...)
  // does nothing (not even call the callback).
  service()->Disconnect(&browser);

  auto wrapper =
      CallbackWrapper(base::BindOnce([](web::proto::WebStateStorage) {}));
  service()->LoadWebStateStorage(&browser, web_state, wrapper.callback());

  EXPECT_FALSE(wrapper.callback_called());
  EXPECT_TRUE(wrapper.callback_destroyed());
}

// Tests that AttachBackup(...) correctly connects the backup Browser to
// the original one, and that changes to any existing WebStates are saved
// to disk even if they happen in the backup Browser.
TEST_F(SessionRestorationServiceImplTest, AttachBackup) {
  // Insert a few WebState in a Browser, wait for the changes to be saved,
  // then destroy the Browser.
  {
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);

    InsertTabsWithUrls(browser, base::make_span(kURLs));
    WaitForSessionSaveComplete();

    service()->Disconnect(&browser);
    WaitForSessionSaveComplete();
  }

  // Create a new Browser and load the session.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);
  service()->LoadSession(&browser);

  SnapshotFiles();

  // Create another Browser and attach it as a backup for `browser`. Check
  // that only the browser metadata file changes when tabs are moved from
  // `browser` to `backup`.
  TestBrowser backup = TestBrowser(browser_state());
  service()->AttachBackup(&browser, &backup);

  // Nothing is saved when attaching the backup.
  WaitForSessionSaveComplete();
  EXPECT_EQ(ModifiedFiles(), FilePathSet{});

  // Moving the WebState should update the session metadata.
  MoveWebStateBetweenWebStateList(browser.GetWebStateList(),
                                  backup.GetWebStateList());
  WaitForSessionSaveComplete();

  EXPECT_EQ(ModifiedFiles(), ExpectedStorageFilesForWebStates(
                                 SessionPathFromIdentifier(kIdentifier0),
                                 /*expect_session_metadata_storage=*/true, {}));

  SnapshotFiles();

  // Force realize a WebState and check that it's state is saved to disk.
  web::WebState* web_state = backup.GetWebStateList()->GetWebStateAt(0);
  {
    base::RunLoop run_loop;
    ScopedTestWebStateObserver web_state_observer(run_loop.QuitClosure());

    web_state_observer.Observe(web_state);
    ASSERT_FALSE(web_state->IsRealized());

    web_state->GetNavigationManager()->LoadIfNecessary();

    run_loop.Run();
  }

  // Check that session metadata storage file and the active WebState storage
  // files are eventually saved.
  WaitForSessionSaveComplete();
  EXPECT_EQ(ModifiedFiles(), ExpectedStorageFilesForWebStates(
                                 SessionPathFromIdentifier(kIdentifier0),
                                 /*expect_session_metadata_storage=*/false,
                                 {WebStateReference{
                                     .web_state = web_state,
                                     .is_native_session_available = true,
                                 }}));

  SnapshotFiles();

  // Check that the WebStates can be moved back to `browser` and that this
  // results in a modification of the WebStateList metadata.
  MoveWebStateBetweenWebStateList(browser.GetWebStateList(),
                                  backup.GetWebStateList());
  WaitForSessionSaveComplete();

  EXPECT_EQ(ModifiedFiles(), ExpectedStorageFilesForWebStates(
                                 SessionPathFromIdentifier(kIdentifier0),
                                 /*expect_session_metadata_storage=*/true, {}));

  // Disconnect the Browsers before destroying the service.
  service()->Disconnect(&backup);
  service()->Disconnect(&browser);
}

// Tests that saving moving a realized WebState between Browser before its
// metadata could be captured in the original Browser does not results in a
// crash. This reproduces the condition for https://crbug.com/329219388 bug.
TEST_F(SessionRestorationServiceImplTest, MoveWebStateWithoutMetadata) {
  TestBrowser browser0 = TestBrowser(browser_state());
  TestBrowser browser1 = TestBrowser(browser_state());

  service()->SetSessionID(&browser0, kIdentifier0);
  service()->SetSessionID(&browser1, kIdentifier1);

  // Create a realized WebState, insert it in `browser0`, then immediately
  // move it to `browser1` without saving the session between the insertion
  // and the move. This means that no metadata will be captured for the
  // WebState, which should not lead to a crash.
  std::unique_ptr<web::WebState> web_state =
      web::WebState::Create(web::WebState::CreateParams(browser_state()));

  // Perform a navigation and wait for it to commit. The reason for the
  // wait is to avoid having a race-condition in the test while ensuring
  // the state has a non-empty navigation history and won't be skipped
  // during the serialization.
  {
    base::RunLoop run_loop;
    ScopedTestWebStateObserver web_state_observer(run_loop.QuitClosure());
    web_state_observer.Observe(web_state.get());

    // The view of the WebState needs to be created before the navigation
    // is really executed.
    std::ignore = web_state->GetView();
    web_state->GetNavigationManager()->LoadURLWithParams(
        web::NavigationManager::WebLoadParams(GURL(kURLs[0])));

    run_loop.Run();
  }

  browser0.GetWebStateList()->InsertWebState(
      std::move(web_state), WebStateList::InsertionParams::Automatic());
  ASSERT_EQ(browser0.GetWebStateList()->count(), 1);

  browser1.GetWebStateList()->InsertWebState(
      browser0.GetWebStateList()->DetachWebStateAt(0),
      WebStateList::InsertionParams::Automatic());

  // This step should not crash.
  WaitForSessionSaveComplete();

  const FilePathSet& expectation0 = ExpectedStorageFilesForBrowser(
      SessionPathFromIdentifier(kIdentifier0), &browser0,
      /*expect_session_metadata_storage=*/true);

  const FilePathSet& expectation1 = ExpectedStorageFilesForBrowser(
      SessionPathFromIdentifier(kIdentifier1), &browser1,
      /*expect_session_metadata_storage=*/true);

  // Both sessions should be saved, and only `browser1` should have data for
  // the WebState (since it was moved from `browser0` before its state could
  // be saved).
  EXPECT_EQ(ModifiedFiles(), expectation0 + expectation1);

  service()->Disconnect(&browser1);
  service()->Disconnect(&browser0);
}

// Tests that LoadDataFromStorage(...) allow extracting data about WebStates
// for a session even if none of the WebState are realized.
TEST_F(SessionRestorationServiceImplTest, LoadDataFromStorage) {
  // Insert a few WebState in a Browser, wait for the changes to be saved,
  // then destroy the Browser. Record the mapping of WebState's identifier
  // to the timestamp of the last navigation.
  WebStateIDToTime expected_times;
  {
    TestBrowser browser = TestBrowser(browser_state());
    service()->SetSessionID(&browser, kIdentifier0);

    InsertTabsWithUrls(browser, base::make_span(kURLs));
    WaitForSessionSaveComplete();

    expected_times = CollectLastCommittedItemTimestampFromWebStateList(
        browser.GetWebStateList());

    service()->Disconnect(&browser);
    WaitForSessionSaveComplete();
  }

  // Create a new Browser and load the session.
  TestBrowser browser = TestBrowser(browser_state());
  service()->SetSessionID(&browser, kIdentifier0);
  service()->LoadSession(&browser);

  // Check that all WebStates are unrealized.
  WebStateList* web_state_list = browser.GetWebStateList();
  const int web_state_list_count = web_state_list->count();
  for (int index = 0; index < web_state_list_count; ++index) {
    web::WebState* web_state = web_state_list->GetWebStateAt(index);
    ASSERT_FALSE(web_state->IsRealized());
  }

  // Asynchronously load the data for the WebState and check that the
  // timestamps are correctly loaded.
  WebStateIDToTime loaded;
  base::RunLoop run_loop;
  service()->LoadDataFromStorage(
      &browser, base::BindRepeating(&GetLastCommittedTimestampFromStorage),
      base::BindOnce(
          [](WebStateIDToTime* loaded, WebStateIDToTime result) {
            *loaded = std::move(result);
          },
          &loaded)
          .Then(run_loop.QuitClosure()));
  run_loop.Run();

  // Check that the data was loaded and is expected.
  EXPECT_EQ(expected_times, loaded);

  // Check that all WebStates are still unrealized.
  for (int index = 0; index < web_state_list_count; ++index) {
    web::WebState* web_state = web_state_list->GetWebStateAt(index);
    EXPECT_FALSE(web_state->IsRealized());
  }

  service()->Disconnect(&browser);
}