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

#import "base/apple/foundation_util.h"
#import "base/containers/span.h"
#import "base/files/scoped_temp_dir.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/time/time.h"
#import "components/tab_groups/tab_group_id.h"
#import "ios/chrome/browser/sessions/model/fake_tab_restore_service.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_tab_group.h"
#import "ios/chrome/browser/sessions/model/session_window_ios.h"
#import "ios/chrome/browser/sessions/model/tab_group_util.h"
#import "ios/web/public/session/crw_navigation_item_storage.h"
#import "ios/web/public/session/crw_session_storage.h"
#import "ios/web/public/session/crw_session_user_data.h"
#import "ios/web/public/session/proto/metadata.pb.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/web_state_id.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "url/gurl.h"

using SessionMigrationTest = PlatformTest;

using tab_groups::TabGroupId;

namespace {

// Information about a single tab.
struct TabInfo {
  const int opener_index = -1;
  const int opener_navigation_index = -1;
  const bool create_web_session = true;
};

// Information about a tab group.
struct TabGroupInfo {
  const int range_start = -1;
  const int range_count = 0;
  const std::u16string title = u"";
  const tab_groups::TabGroupColorId color = tab_groups::TabGroupColorId::kGrey;
  const bool collapsed_state = false;
  const TabGroupId tab_group_id = TabGroupId::GenerateNew();
};

// Information about a session.
struct SessionInfo {
  const int active_index = -1;
  const int pinned_tab_count = 0;
  const base::span<const TabInfo> tabs;
  const base::span<const TabGroupInfo> tab_groups;
};

// Name of the sessions used by the tests (random string obtained by
// running `uuidgen` on the command-line, no meaning to it).
const char kSessionName1[] = "CB5AC1AF-8E93-407B-AE48-65ECE7C241C0";
const char kSessionName2[] = "B829AD40-FD11-4874-B5BF-B56C13DC2496";

// Name of the sub-directory of the off-the-record BrowserState data.
const base::FilePath::CharType kOTRDirectory[] = FILE_PATH_LITERAL("OTR");

// Constants used to populate navigation items.
const char kPageURL[] = "https://www.example.com";
const char kPageTitle[] = "Example Domain";

// Name of unrelated files.
const base::FilePath::CharType kUnrelatedFilename[] =
    FILE_PATH_LITERAL("Tabs_13346873882166178");

// Constants representing the default session used for tests.
constexpr TabInfo kTabs1[] = {
    TabInfo{},
    TabInfo{},
    TabInfo{.create_web_session = false},
    TabInfo{.opener_index = 2, .opener_navigation_index = 0},
    TabInfo{.opener_index = 2, .opener_navigation_index = 0},
};

constexpr SessionInfo kSessionInfo1 = {
    .active_index = 1,
    .pinned_tab_count = 2,
    .tabs = base::make_span(kTabs1),
};

constexpr TabInfo kTabs2[] = {
    TabInfo{},
    TabInfo{},
};

const TabGroupInfo kTabGroups1[] = {
    TabGroupInfo{
        .range_start = 0,
        .range_count = 1,
        .title = u"kTabGroup1",
        .color = tab_groups::TabGroupColorId::kGrey,
    },
};

constexpr SessionInfo kSessionInfo2 = {
    .active_index = 0,
    .pinned_tab_count = 0,
    .tabs = base::make_span(kTabs2),
};

constexpr SessionInfo kSessionWithGroupsInfo = {
    .active_index = 0,
    .pinned_tab_count = 0,
    .tabs = base::make_span(kTabs2),
    .tab_groups = base::make_span(kTabGroups1),
};

// Returns the path to the directory containing the optimized session
// named `name` (located in `root` sub-directory).
base::FilePath GetOptimizedSessionDir(const base::FilePath& root,
                                      const std::string& name) {
  return root.Append(kSessionRestorationDirname).AppendASCII(name);
}

// Returns the path to the directory containing the legacy session
// named `name` (located in `root` sub-directory).
base::FilePath GetLegacySessionDir(const base::FilePath& root,
                                   const std::string& name) {
  return root.Append(kLegacySessionsDirname).AppendASCII(name);
}

// Returns the path to the file containing the web sessions for the
// tab with identifier `identifier` for a legacy session (relative
// to the web session directory `web_sessions`).
base::FilePath GetLegacyWebSessionsFile(const base::FilePath& web_sessions,
                                        web::WebStateID identifier) {
  return web_sessions.Append(
      base::StringPrintf("%08u", identifier.identifier()));
}

// Returns the path to the directory containing the optimized storage
// for a tab named `identifier` for session `session_dir`.
base::FilePath GetOptimizedWebStateDir(const base::FilePath& session_dir,
                                       web::WebStateID identifier) {
  return session_dir.Append(
      base::StringPrintf("%08x", identifier.identifier()));
}

// Converts an active index into a selected index.
NSUInteger SelectedIndexFromActiveIndex(int active_index) {
  return active_index < 0 ? static_cast<NSUInteger>(NSNotFound)
                          : static_cast<NSUInteger>(active_index);
}

// Creates a CRWNavigationItemStorage.
CRWNavigationItemStorage* CreateNavigationItemStorage() {
  CRWNavigationItemStorage* item = [[CRWNavigationItemStorage alloc] init];
  item.title = base::UTF8ToUTF16(&kPageTitle[0]);
  item.virtualURL = GURL(kPageURL);
  return item;
}

// Creates a CRWSessionStorage following `tab_info` and `is_pinned`.
CRWSessionStorage* CreateSessionStorage(TabInfo tab_info, bool is_pinned) {
  CRWSessionUserData* user_data = [[CRWSessionUserData alloc] init];
  if (tab_info.opener_index != -1 && tab_info.opener_navigation_index != -1) {
    [user_data setObject:@(tab_info.opener_index)
                  forKey:kLegacyWebStateListOpenerIndexKey];
    [user_data setObject:@(tab_info.opener_navigation_index)
                  forKey:kLegacyWebStateListOpenerNavigationIndexKey];
  }
  if (is_pinned) {
    [user_data setObject:@YES forKey:kLegacyWebStateListPinnedStateKey];
  }

  CRWSessionStorage* session = [[CRWSessionStorage alloc] init];
  session.stableIdentifier = [[NSUUID UUID] UUIDString];
  session.uniqueIdentifier = web::WebStateID::NewUnique();
  session.itemStorages = @[ CreateNavigationItemStorage() ];
  session.creationTime = base::Time::Now();
  session.lastActiveTime = base::Time::Now();
  session.userData = user_data;
  return session;
}

// Creates a legacy session named `name` following `session_info` and
// writes it to the expected location relative to `root`. It returns
// whether the creation was a success.
bool GenerateLegacySession(const base::FilePath& root,
                           const std::string& name,
                           SessionInfo session_info) {
  const base::FilePath web_sessions = root.Append(kLegacyWebSessionsDirname);

  // Create all the tabs for the session. Note that the WebStateList metadata
  // is stored with the tab in the legacy format.
  NSMutableArray<CRWSessionStorage*>* sessions = [[NSMutableArray alloc] init];
  for (size_t index = 0; index < session_info.tabs.size(); ++index) {
    // Create a fake tab with a single navigation item.
    const bool is_pinned =
        static_cast<int>(index) < session_info.pinned_tab_count;
    CRWSessionStorage* session =
        CreateSessionStorage(session_info.tabs[index], is_pinned);

    // Create fake web session data for the tab. As the file contains
    // opaque data from WebKit, the migration code does not care about
    // the format.
    if (session_info.tabs[index].create_web_session) {
      const base::FilePath filename =
          GetLegacyWebSessionsFile(web_sessions, session.uniqueIdentifier);
      NSData* data = [[NSString stringWithFormat:@"data %zu", index]
          dataUsingEncoding:NSUTF8StringEncoding];
      if (!ios::sessions::WriteFile(filename, data)) {
        return false;
      }
    }

    [sessions addObject:session];
  }

  // Create tab groups for the session.
  NSMutableArray<SessionTabGroup*>* groups = [[NSMutableArray alloc] init];
  for (size_t index = 0; index < session_info.tab_groups.size(); ++index) {
    const TabGroupInfo group_info = session_info.tab_groups[index];
    SessionTabGroup* session_tab_group = [[SessionTabGroup alloc]
        initWithRangeStart:group_info.range_start
                rangeCount:group_info.range_count
                     title:base::SysUTF16ToNSString(group_info.title)
                   colorId:static_cast<NSInteger>(group_info.color)
            collapsedState:group_info.collapsed_state
                tabGroupId:group_info.tab_group_id];
    [groups addObject:session_tab_group];
  }

  const NSUInteger selected_index =
      SelectedIndexFromActiveIndex(session_info.active_index);

  SessionWindowIOS* session =
      [[SessionWindowIOS alloc] initWithSessions:sessions
                                       tabGroups:groups
                                   selectedIndex:selected_index];

  // Write the session file.
  const base::FilePath filename =
      GetLegacySessionDir(root, name).Append(kLegacySessionFilename);
  return ios::sessions::WriteSessionWindow(filename, session);
}

// Creates a legacy session named `name` following `session_info` with
// invalid "unique identifiers" and writes it to the expected location
// relative to `root`. It returns whether the creation was a success.
bool GenerateLegacySessionInvalidUniqueIdentifiers(const base::FilePath& root,
                                                   const std::string& name,
                                                   SessionInfo session_info) {
  const base::FilePath web_sessions = root.Append(kLegacyWebSessionsDirname);

  // Create all the tabs for the session. Note that the WebStateList metadata
  // is stored with the tab in the legacy format.
  NSMutableArray<CRWSessionStorage*>* sessions = [[NSMutableArray alloc] init];
  for (size_t index = 0; index < session_info.tabs.size(); ++index) {
    // Create a fake tab with a single navigation item.
    const bool is_pinned =
        static_cast<int>(index) < session_info.pinned_tab_count;
    CRWSessionStorage* session =
        CreateSessionStorage(session_info.tabs[index], is_pinned);

    // Create fake web session data for the tab. As the file contains
    // opaque data from WebKit, the migration code does not care about
    // the format.
    if (session_info.tabs[index].create_web_session) {
      const base::FilePath filename =
          GetLegacyWebSessionsFile(web_sessions, session.uniqueIdentifier);
      NSData* data = [[NSString stringWithFormat:@"data %zu", index]
          dataUsingEncoding:NSUTF8StringEncoding];
      if (!ios::sessions::WriteFile(filename, data)) {
        return false;
      }
    } else {
      session.uniqueIdentifier = web::WebStateID();  // Clear unique identifier!
    }

    [sessions addObject:session];
  }

  const NSUInteger selected_index =
      SelectedIndexFromActiveIndex(session_info.active_index);

  SessionWindowIOS* session =
      [[SessionWindowIOS alloc] initWithSessions:sessions
                                       tabGroups:@[]
                                   selectedIndex:selected_index];

  // Write the session file.
  const base::FilePath filename =
      GetLegacySessionDir(root, name).Append(kLegacySessionFilename);
  return ios::sessions::WriteSessionWindow(filename, session);
}

// Creates a WebStateMetadataStorage.
web::proto::WebStateMetadataStorage CreateWebStateMetadataStorage() {
  web::proto::WebStateMetadataStorage storage;
  storage.set_navigation_item_count(1);
  storage.mutable_active_page()->set_page_title(kPageTitle);
  storage.mutable_active_page()->set_page_url(kPageURL);
  web::SerializeTimeToProto(base::Time::Now(),
                            *storage.mutable_creation_time());
  web::SerializeTimeToProto(base::Time::Now(),
                            *storage.mutable_last_active_time());
  return storage;
}

// Creates a WebStateStorage.
web::proto::WebStateStorage CreateWebStateStorage() {
  web::proto::NavigationItemStorage item_storage;
  item_storage.set_virtual_url(kPageURL);
  item_storage.set_title(kPageTitle);

  web::proto::WebStateStorage storage;
  storage.mutable_navigation()->set_last_committed_item_index(0);
  *storage.mutable_navigation()->add_items() = item_storage;
  return storage;
}

// Creates an optimized session named `name` following `session_info` and
// writes it to the expected location relative to `root`. It returns
// whether the creation was a success.
bool GenerateOptimizedSession(const base::FilePath& root,
                              const std::string& name,
                              SessionInfo session_info) {
  const base::FilePath session_dir = GetOptimizedSessionDir(root, name);

  ios::proto::WebStateListStorage storage;
  storage.set_active_index(session_info.active_index);
  storage.set_pinned_item_count(session_info.pinned_tab_count);

  // Create all the tabs for the session. Note that the WebStateList metadata
  // is stored with the tab in the legacy format.
  for (size_t index = 0; index < session_info.tabs.size(); ++index) {
    const web::WebStateID identifier = web::WebStateID::NewUnique();
    const base::FilePath item_dir =
        GetOptimizedWebStateDir(session_dir, identifier);
    const TabInfo& tab_info = session_info.tabs[index];

    ios::proto::WebStateListItemStorage& item_storage = *storage.add_items();
    item_storage.set_identifier(identifier.identifier());
    if (tab_info.opener_index != -1 && tab_info.opener_navigation_index != -1) {
      item_storage.mutable_opener()->set_index(tab_info.opener_index);
      item_storage.mutable_opener()->set_navigation_index(
          tab_info.opener_navigation_index);
    }

    // Set the metadata into the WebStateListItemStorage.
    *item_storage.mutable_metadata() = CreateWebStateMetadataStorage();

    // Write the tab data file.
    if (!ios::sessions::WriteProto(item_dir.Append(kWebStateStorageFilename),
                                   CreateWebStateStorage())) {
      return false;
    }

    // Create fake web session data for the tab. As the file contains
    // opaque data from WebKit, the migration code does not care about
    // the format.
    if (tab_info.create_web_session) {
      const base::FilePath filename = item_dir.Append(kWebStateSessionFilename);

      NSData* data = [[NSString stringWithFormat:@"data %zu", index]
          dataUsingEncoding:NSUTF8StringEncoding];
      if (!ios::sessions::WriteFile(filename, data)) {
        return false;
      }
    }
  }

  // Create tab groups for the session.
  for (size_t index = 0; index < session_info.tab_groups.size(); ++index) {
    const TabGroupInfo group_info = session_info.tab_groups[index];

    ios::proto::TabGroupStorage& group_storage = *storage.add_groups();
    ios::proto::RangeIndex& range = *group_storage.mutable_range();

    range.set_start(group_info.range_start);
    range.set_count(group_info.range_count);

    group_storage.set_title(base::UTF16ToUTF8(group_info.title));
    group_storage.set_color(tab_group_util::ColorForStorage(group_info.color));
    group_storage.set_collapsed(group_info.collapsed_state);
    tab_group_util::TabGroupIdForStorage(group_info.tab_group_id,
                                         *group_storage.mutable_tab_group_id());
  }

  // Write the session metadata file.
  const base::FilePath filename = session_dir.Append(kSessionMetadataFilename);
  return ios::sessions::WriteProto(filename, storage);
}

// Checks whether the optimized session in `root` named `name` corresponds
// to the session described by `session_info`.
void CheckOptimizedSession(const base::FilePath& root,
                           const std::string& name,
                           SessionInfo session_info) {
  const base::FilePath session_dir = GetOptimizedSessionDir(root, name);

  // Load the optimized session from disk.
  ios::proto::WebStateListStorage storage;
  ASSERT_TRUE(ios::sessions::ParseProto(
      session_dir.Append(kSessionMetadataFilename), storage));

  // Check that the session metadata are correct.
  EXPECT_EQ(storage.active_index(), session_info.active_index);
  EXPECT_EQ(storage.pinned_item_count(), session_info.pinned_tab_count);

  // Check that each tab metadata and data are correct.
  ASSERT_EQ(storage.items_size(), static_cast<int>(session_info.tabs.size()));
  for (size_t index = 0; index < session_info.tabs.size(); ++index) {
    const ios::proto::WebStateListItemStorage& item_info = storage.items(index);
    const TabInfo& tab_info = session_info.tabs[index];

    // Check the opener-opener relationship for correctness.
    if (tab_info.opener_index != -1 && tab_info.opener_navigation_index != -1) {
      EXPECT_TRUE(item_info.has_opener());
      const ios::proto::OpenerStorage& opener = item_info.opener();
      EXPECT_EQ(opener.index(), tab_info.opener_index);
      EXPECT_EQ(opener.navigation_index(), tab_info.opener_navigation_index);
    } else {
      EXPECT_FALSE(item_info.has_opener());
    }

    // Check the tab metadata for correctness.
    ASSERT_TRUE(item_info.has_metadata());
    const web::proto::WebStateMetadataStorage& metadata = item_info.metadata();

    EXPECT_EQ(metadata.navigation_item_count(), 1);
    EXPECT_NE(web::TimeFromProto(metadata.creation_time()), base::Time());
    EXPECT_NE(web::TimeFromProto(metadata.last_active_time()), base::Time());
    EXPECT_TRUE(metadata.has_active_page());
    EXPECT_EQ(metadata.active_page().page_title(), kPageTitle);
    EXPECT_EQ(GURL(metadata.active_page().page_url()), GURL(kPageURL));

    ASSERT_TRUE(web::WebStateID::IsValidValue(item_info.identifier()));
    const base::FilePath item_dir = GetOptimizedWebStateDir(
        session_dir,
        web::WebStateID::FromSerializedValue(item_info.identifier()));

    // Check the tab data for correctness.
    web::proto::WebStateStorage item_storage;
    ASSERT_TRUE(ios::sessions::ParseProto(
        item_dir.Append(kWebStateStorageFilename), item_storage));

    EXPECT_FALSE(item_storage.has_metadata());
    EXPECT_EQ(item_storage.navigation().last_committed_item_index(), 0);
    ASSERT_EQ(item_storage.navigation().items_size(), 1);

    const web::proto::NavigationItemStorage& navigation_item =
        item_storage.navigation().items(0);
    EXPECT_EQ(navigation_item.title(), kPageTitle);
    EXPECT_EQ(GURL(navigation_item.virtual_url()), GURL(kPageURL));

    // Check that the native session file exists if created.
    EXPECT_EQ(
        ios::sessions::FileExists(item_dir.Append(kWebStateSessionFilename)),
        tab_info.create_web_session);
  }

  for (size_t index = 0; index < session_info.tab_groups.size(); ++index) {
    const ios::proto::TabGroupStorage& group_storage = storage.groups(index);
    const TabGroupInfo& group_info = session_info.tab_groups[index];

    EXPECT_EQ(group_storage.range().start(), group_info.range_start);
    EXPECT_EQ(group_storage.range().count(), group_info.range_count);
    EXPECT_EQ(group_storage.title(), base::UTF16ToUTF8(group_info.title));
    EXPECT_EQ(group_storage.color(),
              tab_group_util::ColorForStorage(group_info.color));
    EXPECT_EQ(group_storage.collapsed(), group_info.collapsed_state);
    EXPECT_EQ(
        tab_group_util::TabGroupIdFromStorage(group_storage.tab_group_id()),
        group_info.tab_group_id);
  }
}

// Checks whether the legacy session in `root` named `name` corresponds
// to the session described by `session_info`.
void CheckLegacySession(const base::FilePath& root,
                        const std::string& name,
                        SessionInfo session_info) {
  const base::FilePath session_dir = GetLegacySessionDir(root, name);
  const base::FilePath web_sessions = root.Append(kLegacyWebSessionsDirname);

  // Load the legacy session from disk.
  SessionWindowIOS* session_window = ios::sessions::ReadSessionWindow(
      session_dir.Append(kLegacySessionFilename));
  ASSERT_TRUE(session_window);

  EXPECT_EQ(session_window.selectedIndex,
            SelectedIndexFromActiveIndex(session_info.active_index));

  // Check that the information for each tab is correct.
  ASSERT_EQ(session_window.sessions.count, session_info.tabs.size());
  for (size_t index = 0; index < session_info.tabs.size(); ++index) {
    CRWSessionStorage* session = session_window.sessions[index];
    const TabInfo& tab_info = session_info.tabs[index];

    // Check the opener-opener relationship for correctness.
    CRWSessionUserData* user_data = session.userData;
    if (tab_info.opener_index != -1 && tab_info.opener_navigation_index != -1) {
      EXPECT_NSEQ(base::apple::ObjCCast<NSNumber>([user_data
                      objectForKey:kLegacyWebStateListOpenerIndexKey]),
                  @(tab_info.opener_index));
      EXPECT_NSEQ(
          base::apple::ObjCCast<NSNumber>([user_data
              objectForKey:kLegacyWebStateListOpenerNavigationIndexKey]),
          @(tab_info.opener_navigation_index));
    }

    // Check the pinned status for correctness.
    if (static_cast<int>(index) < session_info.pinned_tab_count) {
      EXPECT_NSEQ(base::apple::ObjCCast<NSNumber>([user_data
                      objectForKey:kLegacyWebStateListPinnedStateKey]),
                  @YES);
    } else {
      EXPECT_NSEQ(base::apple::ObjCCast<NSNumber>([user_data
                      objectForKey:kLegacyWebStateListPinnedStateKey]),
                  nil);
    }

    // Check that a stable identifier was generated (randomized).
    EXPECT_TRUE(session.stableIdentifier);
    EXPECT_NE(session.uniqueIdentifier, web::WebStateID());

    // Check the tab data for correctness.
    EXPECT_EQ(session.lastCommittedItemIndex, 0);
    EXPECT_NE(session.creationTime, base::Time());
    EXPECT_NE(session.lastActiveTime, base::Time());
    ASSERT_EQ(session.itemStorages.count, 1u);

    CRWNavigationItemStorage* navigation_item = session.itemStorages[0];
    EXPECT_EQ(base::UTF16ToUTF8(navigation_item.title), kPageTitle);
    EXPECT_EQ(navigation_item.virtualURL, GURL(kPageURL));

    // Check that the native session file exists if created.
    EXPECT_EQ(ios::sessions::FileExists(GetLegacyWebSessionsFile(
                  web_sessions, session.uniqueIdentifier)),
              tab_info.create_web_session);
  }

  for (size_t index = 0; index < session_info.tab_groups.size(); ++index) {
    SessionTabGroup* group_session = session_window.tabGroups[index];
    const TabGroupInfo& group_info = session_info.tab_groups[index];

    EXPECT_EQ(group_session.rangeStart, group_info.range_start);
    EXPECT_EQ(group_session.rangeCount, group_info.range_count);
    EXPECT_EQ(base::SysNSStringToUTF16(group_session.title), group_info.title);
    EXPECT_EQ(group_session.colorId, static_cast<int>(group_info.color));
    EXPECT_EQ(group_session.collapsedState, group_info.collapsed_state);
    EXPECT_EQ(group_session.tabGroupId, group_info.tab_group_id);
  }
}

}  // namespace

// Tests batch migrating sessions from legacy to optimized works correctly.
TEST_F(SessionMigrationTest, BatchToOptimized) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few legacy sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateLegacySession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(GenerateLegacySession(root, kSessionName2, kSessionInfo2));
  EXPECT_TRUE(GenerateLegacySession(otr, kSessionName1, SessionInfo()));

  // Check that the migration is a success.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(
      ios::sessions::MigrationResult::Success(identifier),
      ios::sessions::MigrateSessionsInPathsToOptimized(paths, identifier));

  // Check that the directories containing legacy sessions and WKWebView
  // native session data have been deleted in all paths.
  for (const base::FilePath& path : paths) {
    const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(legacy_dir));

    const base::FilePath native_dir = path.Append(kLegacyWebSessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(native_dir));
  }

  // Check that the sessions have been correctly converted.
  CheckOptimizedSession(root, kSessionName1, kSessionInfo1);
  CheckOptimizedSession(root, kSessionName2, kSessionInfo2);
  CheckOptimizedSession(otr, kSessionName1, SessionInfo());
}

// Tests batch migrating sessions from legacy to optimized works correctly
// when there are no sessions.
TEST_F(SessionMigrationTest, BatchToOptimized_NoSession) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Check that the migration is a success.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(
      ios::sessions::MigrationResult::Success(identifier),
      ios::sessions::MigrateSessionsInPathsToOptimized(paths, identifier));

  // Check that the optimized session directories have not been created.
  for (const base::FilePath& path : paths) {
    const base::FilePath optimized_dir =
        path.Append(kSessionRestorationDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(optimized_dir));
  }
}

// Tests batch migrating sessions from legacy to optimized works correctly
// when there are no sessions but empty directory laying around.
TEST_F(SessionMigrationTest, BatchToOptimized_NoSessionEmptyDirs) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Create empty directories.
  for (const base::FilePath& path : paths) {
    using ios::sessions::CreateDirectory;
    ASSERT_TRUE(CreateDirectory(GetLegacySessionDir(path, kSessionName1)));
    ASSERT_TRUE(CreateDirectory(GetLegacySessionDir(path, kSessionName2)));
    ASSERT_TRUE(CreateDirectory(path.Append(kLegacyWebSessionsDirname)));
  }

  // Check that the migration is a success.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(
      ios::sessions::MigrationResult::Success(identifier),
      ios::sessions::MigrateSessionsInPathsToOptimized(paths, identifier));

  // Check that the directories containing legacy sessions and WKWebView
  // native session data have been deleted in all paths.
  for (const base::FilePath& path : paths) {
    const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(legacy_dir));

    const base::FilePath native_dir = path.Append(kLegacyWebSessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(native_dir));
  }

  // Check that the optimized session directories have not been created.
  for (const base::FilePath& path : paths) {
    const base::FilePath optimized_dir =
        path.Append(kSessionRestorationDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(optimized_dir));
  }
}

// Tests batch migrating sessions from legacy to optimized works correctly
// and does nothing if the sessions are already in the optimized format.
TEST_F(SessionMigrationTest, BatchToOptimized_SessionAreOptimized) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few optimized sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateOptimizedSession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(GenerateOptimizedSession(root, kSessionName2, kSessionInfo2));
  EXPECT_TRUE(GenerateOptimizedSession(otr, kSessionName1, SessionInfo()));

  // Check that the migration is a success.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(
      ios::sessions::MigrationResult::Success(identifier),
      ios::sessions::MigrateSessionsInPathsToOptimized(paths, identifier));

  // Check that the sessions have been left untouched.
  CheckOptimizedSession(root, kSessionName1, kSessionInfo1);
  CheckOptimizedSession(root, kSessionName2, kSessionInfo2);
  CheckOptimizedSession(otr, kSessionName1, SessionInfo());
}

// Tests batch migrating sessions from legacy to optimized works correctly
// and leave unrelated files in the legacy session directories untouched.
TEST_F(SessionMigrationTest, BatchToOptimized_UnrelatedFilesUnaffected) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few legacy sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateLegacySession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(GenerateLegacySession(root, kSessionName2, kSessionInfo2));
  EXPECT_TRUE(GenerateLegacySession(otr, kSessionName1, SessionInfo()));

  // Create a few unrelated files in the legacy session directories (they
  // corresponds to files saved by //components/sessions).
  NSData* data = [@"data" dataUsingEncoding:NSUTF8StringEncoding];
  for (const base::FilePath& path : paths) {
    const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
    const base::FilePath filename = legacy_dir.Append(kUnrelatedFilename);
    EXPECT_TRUE(ios::sessions::WriteFile(filename, data));
  }

  // Check that the migration is a success.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(
      ios::sessions::MigrationResult::Success(identifier),
      ios::sessions::MigrateSessionsInPathsToOptimized(paths, identifier));

  // Check that the directories containing legacy sessions and WKWebView
  // native session data have been deleted in all paths.
  for (const base::FilePath& path : paths) {
    const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
    const base::FilePath filename = legacy_dir.Append(kUnrelatedFilename);
    EXPECT_TRUE(ios::sessions::DirectoryExists(legacy_dir));
    EXPECT_NSEQ(ios::sessions::ReadFile(filename), data);

    const base::FilePath native_dir = path.Append(kLegacyWebSessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(native_dir));
  }

  // Check that the sessions have been correctly converted.
  CheckOptimizedSession(root, kSessionName1, kSessionInfo1);
  CheckOptimizedSession(root, kSessionName2, kSessionInfo2);
  CheckOptimizedSession(otr, kSessionName1, SessionInfo());
}

// Tests batch migrating sessions from legacy to optimized works correctly
// when unique identifiers are invalid and assign new unique identifier.
TEST_F(SessionMigrationTest, BatchToOptimized_InvalidUniqueIdentifiers) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few legacy sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateLegacySessionInvalidUniqueIdentifiers(root, kSessionName1,
                                                            kSessionInfo1));
  EXPECT_TRUE(GenerateLegacySession(root, kSessionName2, kSessionInfo2));
  EXPECT_TRUE(GenerateLegacySession(otr, kSessionName1, SessionInfo()));

  // Check that the migration is a success and that the tabs from both
  // sessions without identifiers have been assigned new identifiers.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  const int32_t expected_idenfifier =
      identifier +
      static_cast<int>(std::count_if(
          kSessionInfo1.tabs.begin(), kSessionInfo1.tabs.end(),
          [](const TabInfo& tab) { return !tab.create_web_session; }));

  ASSERT_NE(identifier, expected_idenfifier);
  ASSERT_EQ(
      ios::sessions::MigrationResult::Success(expected_idenfifier),
      ios::sessions::MigrateSessionsInPathsToOptimized(paths, identifier));

  // Check that the directories containing legacy sessions and WKWebView
  // native session data have been deleted in all paths.
  for (const base::FilePath& path : paths) {
    const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(legacy_dir));

    const base::FilePath native_dir = path.Append(kLegacyWebSessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(native_dir));
  }

  // Check that the sessions have been correctly converted.
  CheckOptimizedSession(root, kSessionName1, kSessionInfo1);
  CheckOptimizedSession(root, kSessionName2, kSessionInfo2);
  CheckOptimizedSession(otr, kSessionName1, SessionInfo());
}

// Tests batch migrating sessions from legacy to optimized when one session
// is invalid and cannot be loaded.
TEST_F(SessionMigrationTest, BatchToOptimized_FailureInvalidSessions) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few legacy sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateLegacySession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(GenerateLegacySession(otr, kSessionName1, SessionInfo()));

  // Write invalid data in one sessions.
  const base::FilePath session_path =
      GetLegacySessionDir(root, kSessionName2).Append(kLegacySessionFilename);
  NSData* data = [@"data" dataUsingEncoding:NSUTF8StringEncoding];
  EXPECT_TRUE(ios::sessions::WriteFile(session_path, data));

  // Check that the migration is a failure.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(
      ios::sessions::MigrationResult::Failure(),
      ios::sessions::MigrateSessionsInPathsToOptimized(paths, identifier));

  // Check that the legacy sessions have been left untouched, including the
  // invalid session.
  CheckLegacySession(root, kSessionName1, kSessionInfo1);
  CheckLegacySession(otr, kSessionName1, SessionInfo());
  EXPECT_NSEQ(ios::sessions::ReadFile(session_path), data);

  // Check that the optimized session directory was not created.
  for (const base::FilePath& path : paths) {
    const base::FilePath optimized_dir =
        path.Append(kSessionRestorationDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(optimized_dir));
  }
}

// Tests batch migrating sessions from legacy to optimized when one session
// cannot be migrated.
TEST_F(SessionMigrationTest, BatchToOptimized_FailureMigration) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few legacy sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateLegacySession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(GenerateLegacySession(root, kSessionName2, kSessionInfo2));
  EXPECT_TRUE(GenerateLegacySession(otr, kSessionName1, SessionInfo()));

  // Create a file with the same name as one of the optimized session
  // directory which should prevent migrating the sessions.
  NSData* data = [@"data" dataUsingEncoding:NSUTF8StringEncoding];
  const base::FilePath dest_dir = GetOptimizedSessionDir(root, kSessionName2);
  EXPECT_TRUE(ios::sessions::WriteFile(dest_dir, data));

  // Check that the migration is a failure.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(
      ios::sessions::MigrationResult::Failure(),
      ios::sessions::MigrateSessionsInPathsToOptimized(paths, identifier));

  // Check that the legacy sessions have been left untouched.
  CheckLegacySession(root, kSessionName1, kSessionInfo1);
  CheckLegacySession(root, kSessionName2, kSessionInfo2);
  CheckLegacySession(otr, kSessionName1, SessionInfo());

  // Check that the optimized session directory was not created, and
  // that any pre-existing data was deleted.
  for (const base::FilePath& path : paths) {
    const base::FilePath optimized_dir =
        path.Append(kSessionRestorationDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(optimized_dir));
  }
}

// Tests batch migrating sessions from optimized to legacy works correctly.
TEST_F(SessionMigrationTest, BatchToLegacy) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few optimized sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateOptimizedSession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(GenerateOptimizedSession(root, kSessionName2, kSessionInfo2));
  EXPECT_TRUE(GenerateOptimizedSession(otr, kSessionName1, SessionInfo()));

  // Check that the migration is a success.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(ios::sessions::MigrationResult::Success(identifier),
            ios::sessions::MigrateSessionsInPathsToLegacy(paths, identifier));

  // Check that the directories containing optimized sessions have been
  // deleted in all paths.
  for (const base::FilePath& path : paths) {
    const base::FilePath optimized_dir =
        path.Append(kSessionRestorationDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(optimized_dir));
  }

  // Check that the sessions have been correctly converted.
  CheckLegacySession(root, kSessionName1, kSessionInfo1);
  CheckLegacySession(root, kSessionName2, kSessionInfo2);
  CheckLegacySession(otr, kSessionName1, SessionInfo());
}

// Tests batch migrating sessions from optimized to legacy works correctly
// when there are no sessions.
TEST_F(SessionMigrationTest, BatchToLegacy_NoSession) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Check that the migration is a success.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(ios::sessions::MigrationResult::Success(identifier),
            ios::sessions::MigrateSessionsInPathsToLegacy(paths, identifier));

  // Check that the legacy session and WKWebView native session data
  // directories have not been created.
  for (const base::FilePath& path : paths) {
    const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(legacy_dir));

    const base::FilePath native_dir = path.Append(kLegacyWebSessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(native_dir));
  }
}

// Tests batch migrating sessions from optimized to legacy works correctly
// when there are no sessions but empty directory laying around.
TEST_F(SessionMigrationTest, BatchToLegacy_NoSessionEmptyDirs) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Create empty directories.
  for (const base::FilePath& path : paths) {
    using ios::sessions::CreateDirectory;
    ASSERT_TRUE(CreateDirectory(GetOptimizedSessionDir(path, kSessionName1)));
    ASSERT_TRUE(CreateDirectory(GetOptimizedSessionDir(path, kSessionName2)));
  }

  // Check that the migration is a success.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(ios::sessions::MigrationResult::Success(identifier),
            ios::sessions::MigrateSessionsInPathsToLegacy(paths, identifier));

  // Check that the directories containing optimized sessions have been
  // deleted in all paths.
  for (const base::FilePath& path : paths) {
    const base::FilePath optimized_dir =
        path.Append(kSessionRestorationDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(optimized_dir));
  }

  // Check that the legacy session and WKWebView native session data
  // directories have not been created.
  for (const base::FilePath& path : paths) {
    const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(legacy_dir));

    const base::FilePath native_dir = path.Append(kLegacyWebSessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(native_dir));
  }
}

// Tests batch migrating sessions from optimized to legacy works correctly
// and does nothing if the sessions are already in the legacy format.
TEST_F(SessionMigrationTest, BatchToLegacy_SessionAreLegacy) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few legacy sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateLegacySession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(GenerateLegacySession(root, kSessionName2, kSessionInfo2));
  EXPECT_TRUE(GenerateLegacySession(otr, kSessionName1, SessionInfo()));

  // Check that the migration is a success.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(ios::sessions::MigrationResult::Success(identifier),
            ios::sessions::MigrateSessionsInPathsToLegacy(paths, identifier));

  // Check that the sessions have been left untouched.
  CheckLegacySession(root, kSessionName1, kSessionInfo1);
  CheckLegacySession(root, kSessionName2, kSessionInfo2);
  CheckLegacySession(otr, kSessionName1, SessionInfo());
}

// Tests batch migrating sessions from optimized to legacy when one session
// is invalid and cannot be loaded.
TEST_F(SessionMigrationTest, BatchToLegacy_FailureInvalidSessions) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few optimized sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateOptimizedSession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(GenerateOptimizedSession(otr, kSessionName1, SessionInfo()));

  // Write invalid data in one sessions.
  const base::FilePath session_path =
      GetOptimizedSessionDir(root, kSessionName2)
          .Append(kSessionMetadataFilename);
  NSData* data = [@"data" dataUsingEncoding:NSUTF8StringEncoding];
  EXPECT_TRUE(ios::sessions::WriteFile(session_path, data));

  // Check that the migration is a failure.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(ios::sessions::MigrationResult::Failure(),
            ios::sessions::MigrateSessionsInPathsToLegacy(paths, identifier));

  // Check that the optimized sessions have been left untouched, including the
  // invalid session.
  CheckOptimizedSession(root, kSessionName1, kSessionInfo1);
  CheckOptimizedSession(otr, kSessionName1, SessionInfo());
  EXPECT_NSEQ(ios::sessions::ReadFile(session_path), data);

  // Check that the legacy session and WKWebView native session data
  // directories have not been created.
  for (const base::FilePath& path : paths) {
    const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(legacy_dir));

    const base::FilePath native_dir = path.Append(kLegacyWebSessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(native_dir));
  }
}

// Tests batch migrating sessions from optimized to legacy when one session
// cannot be migrated.
TEST_F(SessionMigrationTest, BatchToLegacy_FailureMigration) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few optimized sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateOptimizedSession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(GenerateOptimizedSession(root, kSessionName2, kSessionInfo2));
  EXPECT_TRUE(GenerateOptimizedSession(otr, kSessionName1, SessionInfo()));

  // Create a file with the same name as one of the legacy session
  // directory which should prevent migrating the sessions.
  NSData* data = [@"data" dataUsingEncoding:NSUTF8StringEncoding];
  const base::FilePath dest_dir = GetLegacySessionDir(otr, kSessionName1);
  EXPECT_TRUE(ios::sessions::WriteFile(dest_dir, data));

  // Check that the migration is a failure.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(ios::sessions::MigrationResult::Failure(),
            ios::sessions::MigrateSessionsInPathsToLegacy(paths, identifier));

  // Check that the optimized sessions have been left untouched.
  CheckOptimizedSession(root, kSessionName1, kSessionInfo1);
  CheckOptimizedSession(root, kSessionName2, kSessionInfo2);
  CheckOptimizedSession(otr, kSessionName1, SessionInfo());

  // Check that the legacy session and WKWebView native session data
  // directories have not been created and that any pre-existing data
  // was deleted.
  for (const base::FilePath& path : paths) {
    if (path != otr) {
      const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
      EXPECT_FALSE(ios::sessions::DirectoryExists(legacy_dir));
    }

    const base::FilePath native_dir = path.Append(kLegacyWebSessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(native_dir));
  }

  // Check that the pre-existing invalid file is still present.
  EXPECT_NSEQ(ios::sessions::ReadFile(dest_dir), data);
}

// Tests batch migrating sessions from optimized to legacy when one session
// cannot be migrated, with unrelated files in the legacy session directory
// which should be unaffected.
TEST_F(SessionMigrationTest, BatchToLegacy_FailureUnrelatedFilesUnaffected) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few optimized sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateOptimizedSession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(GenerateOptimizedSession(root, kSessionName2, kSessionInfo2));
  EXPECT_TRUE(GenerateOptimizedSession(otr, kSessionName1, SessionInfo()));

  // Create a few unrelated files in the legacy session directories (they
  // corresponds to files saved by //components/sessions).
  NSData* data = [@"data" dataUsingEncoding:NSUTF8StringEncoding];
  for (const base::FilePath& path : paths) {
    const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
    const base::FilePath filename = legacy_dir.Append(kUnrelatedFilename);
    EXPECT_TRUE(ios::sessions::WriteFile(filename, data));
  }

  // Write invalid data in one sessions.
  const base::FilePath session_path = GetOptimizedSessionDir(otr, kSessionName2)
                                          .Append(kSessionMetadataFilename);
  EXPECT_TRUE(ios::sessions::WriteFile(session_path, data));

  // Check that the migration is a failure.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(ios::sessions::MigrationResult::Failure(),
            ios::sessions::MigrateSessionsInPathsToLegacy(paths, identifier));

  // Check that the optimized sessions have been left untouched, including
  // the invalid session.
  CheckOptimizedSession(root, kSessionName1, kSessionInfo1);
  CheckOptimizedSession(root, kSessionName2, kSessionInfo2);
  CheckOptimizedSession(otr, kSessionName1, SessionInfo());
  EXPECT_NSEQ(ios::sessions::ReadFile(session_path), data);

  // Check that the legacy session and WKWebView native session data
  // directories have not been created and that any pre-existing data
  // was deleted.
  for (const base::FilePath& path : paths) {
    const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
    const base::FilePath filename = legacy_dir.Append(kUnrelatedFilename);
    EXPECT_TRUE(ios::sessions::DirectoryExists(legacy_dir));
    EXPECT_NSEQ(ios::sessions::ReadFile(filename), data);

    const base::FilePath native_dir = path.Append(kLegacyWebSessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(native_dir));
  }
}

// Tests batch migrating sessions with tab groups from legacy to optimized works
// correctly.
TEST_F(SessionMigrationTest, BatchToOptimizedWithGroups) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few legacy sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateLegacySession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(
      GenerateLegacySession(root, kSessionName2, kSessionWithGroupsInfo));
  EXPECT_TRUE(GenerateLegacySession(otr, kSessionName1, SessionInfo()));

  // Check that the migration is a success.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(
      ios::sessions::MigrationResult::Success(identifier),
      ios::sessions::MigrateSessionsInPathsToOptimized(paths, identifier));

  // Check that the directories containing legacy sessions and WKWebView
  // native session data have been deleted in all paths.
  for (const base::FilePath& path : paths) {
    const base::FilePath legacy_dir = path.Append(kLegacySessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(legacy_dir));

    const base::FilePath native_dir = path.Append(kLegacyWebSessionsDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(native_dir));
  }

  // Check that the sessions have been correctly converted.
  CheckOptimizedSession(root, kSessionName1, kSessionInfo1);
  CheckOptimizedSession(root, kSessionName2, kSessionWithGroupsInfo);
  CheckOptimizedSession(otr, kSessionName1, SessionInfo());
}

// Tests batch migrating sessions from optimized to legacy works correctly.
TEST_F(SessionMigrationTest, BatchToLegacyWithGroups) {
  base::ScopedTempDir scoped_temp_dir;
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  const base::FilePath root = scoped_temp_dir.GetPath();
  const base::FilePath otr = root.Append(kOTRDirectory);
  const std::vector<base::FilePath> paths{root, otr};

  // Generate a few optimized sessions for the main and OTR BrowserStates.
  EXPECT_TRUE(GenerateOptimizedSession(root, kSessionName1, kSessionInfo1));
  EXPECT_TRUE(
      GenerateOptimizedSession(root, kSessionName2, kSessionWithGroupsInfo));
  EXPECT_TRUE(GenerateOptimizedSession(otr, kSessionName1, SessionInfo()));

  // Check that the migration is a success.
  const int32_t identifier = web::WebStateID::NewUnique().identifier();
  ASSERT_EQ(ios::sessions::MigrationResult::Success(identifier),
            ios::sessions::MigrateSessionsInPathsToLegacy(paths, identifier));

  // Check that the directories containing optimized sessions have been
  // deleted in all paths.
  for (const base::FilePath& path : paths) {
    const base::FilePath optimized_dir =
        path.Append(kSessionRestorationDirname);
    EXPECT_FALSE(ios::sessions::DirectoryExists(optimized_dir));
  }

  // Check that the sessions have been correctly converted.
  CheckLegacySession(root, kSessionName1, kSessionInfo1);
  CheckLegacySession(root, kSessionName2, kSessionWithGroupsInfo);
  CheckLegacySession(otr, kSessionName1, SessionInfo());
}