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

#include "chrome/browser/downgrade/snapshot_manager.h"

#include <ranges>

#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/downgrade/downgrade_manager_delegate.h"
#include "chrome/browser/downgrade/downgrade_utils.h"
#include "chrome/browser/downgrade/snapshot_file_collector.h"
#include "chrome/browser/downgrade/user_data_downgrade.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace downgrade {

namespace {

constexpr base::FilePath::StringViewType kSomeFolder =
    FILE_PATH_LITERAL("Some Folder");
constexpr base::FilePath::StringViewType kSomeFile =
    FILE_PATH_LITERAL("Some File");
constexpr base::FilePath::StringViewType kSomeFolderFile =
    FILE_PATH_LITERAL("A File");
constexpr base::FilePath::StringViewType kSomeSubFolder =
    FILE_PATH_LITERAL("Some Sub Folder");
constexpr base::FilePath::StringViewType kSomeSubFile =
    FILE_PATH_LITERAL("Some Sub File");

constexpr base::FilePath::StringViewType kUserDataFolder =
    FILE_PATH_LITERAL("User Data Folder");
constexpr base::FilePath::StringViewType kProfileDataFolder =
    FILE_PATH_LITERAL("Profile Data Folder");
constexpr base::FilePath::StringViewType kUserDataFile =
    FILE_PATH_LITERAL("User Data File");
constexpr base::FilePath::StringViewType kProfileDataFile =
    FILE_PATH_LITERAL("Profile Data File");
constexpr base::FilePath::StringViewType kProfileDataJournalFile =
    FILE_PATH_LITERAL("Profile Data File-journal");
constexpr base::FilePath::StringViewType kProfileDataExtFile =
    FILE_PATH_LITERAL("Profile Data File.ext");
constexpr base::FilePath::StringViewType kProfileDataExtWalFile =
    FILE_PATH_LITERAL("Profile Data File.ext-wal");
constexpr base::FilePath::StringViewType kProfileDataExtShmFile =
    FILE_PATH_LITERAL("Profile Data File.ext-shm");

constexpr std::array<base::FilePath::StringViewType, 3>
    kProfileDirectoryBaseNames = {FILE_PATH_LITERAL("Default"),
                                  FILE_PATH_LITERAL("Profile 1"),
                                  FILE_PATH_LITERAL("Profile 2")};

// Structure containing a folders and files structure for tests.
// root
// |_ SomeFile
// |_ Some Folder
//    |_ Some File
//    |_ Some Sub Folder
//       |_ Some Sub File
class TestFolderAndFiles {
 public:
  // Creates the files and folders under the provided |root|.
  // Cleanup must be done manually.
  static void CreateFilesAndFolders(const base::FilePath& root) {
    TestFolderAndFiles folders_and_files(root);
    ASSERT_TRUE(base::CreateDirectory(folders_and_files.some_sub_folder_path_));
    base::File(folders_and_files.some_file_path_,
               base::File::FLAG_CREATE | base::File::FLAG_WRITE);
    base::File(folders_and_files.some_folder_file_path_,
               base::File::FLAG_CREATE | base::File::FLAG_WRITE);
    base::File(folders_and_files.some_sub_folder_file_path_,
               base::File::FLAG_CREATE | base::File::FLAG_WRITE);
  }

  static bool AllPathExists(const base::FilePath& root) {
    TestFolderAndFiles folders_and_files(root);
    return base::DirectoryExists(folders_and_files.some_folder_path_) &&
           base::DirectoryExists(folders_and_files.some_sub_folder_path_) &&
           base::PathExists(folders_and_files.some_file_path_) &&
           base::PathExists(folders_and_files.some_folder_file_path_) &&
           base::PathExists(folders_and_files.some_sub_folder_file_path_);
  }

  static bool NoPathExists(const base::FilePath& root) {
    TestFolderAndFiles folders_and_files(root);
    return !base::DirectoryExists(folders_and_files.some_folder_path_) &&
           !base::DirectoryExists(folders_and_files.some_sub_folder_path_) &&
           !base::PathExists(folders_and_files.some_file_path_) &&
           !base::PathExists(folders_and_files.some_folder_file_path_) &&
           !base::PathExists(folders_and_files.some_sub_folder_file_path_);
  }

 private:
  explicit TestFolderAndFiles(const base::FilePath& root) {
    some_folder_path_ = root.Append(kSomeFolder);
    some_folder_file_path_ = some_folder_path_.Append(kSomeFile);
    some_file_path_ = some_folder_path_.Append(kSomeFolderFile);
    some_sub_folder_path_ = some_folder_path_.Append(kSomeSubFolder);
    some_sub_folder_file_path_ = some_sub_folder_path_.Append(kSomeSubFile);
  }
  ~TestFolderAndFiles() = default;

  base::FilePath some_folder_path_;
  base::FilePath some_file_path_;
  base::FilePath some_folder_file_path_;
  base::FilePath some_sub_folder_path_;
  base::FilePath some_sub_folder_file_path_;
};

}  // namespace

class TestDowngradeManagerDelegate : public DowngradeManagerDelegate {
 public:
  TestDowngradeManagerDelegate() = default;
  ~TestDowngradeManagerDelegate() override = default;

  int GetMaxNumberOfSnapshots() const override { return 3; }
  bool UserDataSnapshotEnabled() const override { return true; }
  base::FilePath GetDiskCacheDir() const override { return base::FilePath(); }

  std::vector<SnapshotItemDetails> GetUserDataSnapshotItems() const override {
    return std::vector<SnapshotItemDetails>{
        SnapshotItemDetails(base::FilePath(kUserDataFile),
                            SnapshotItemDetails::ItemType::kFile, 0),
        SnapshotItemDetails(base::FilePath(kUserDataFolder),
                            SnapshotItemDetails::ItemType::kDirectory, 0)};
  }
  std::vector<SnapshotItemDetails> GetProfileSnapshotItems() const override {
    return std::vector<SnapshotItemDetails>{
        SnapshotItemDetails(base::FilePath(kProfileDataFile),
                            SnapshotItemDetails::ItemType::kFile, 0),
        SnapshotItemDetails(base::FilePath(kProfileDataExtFile),
                            SnapshotItemDetails::ItemType::kFile, 0),
        SnapshotItemDetails(base::FilePath(kProfileDataFolder),
                            SnapshotItemDetails::ItemType::kDirectory, 0)};
  }
};

class SnapshotManagerTest : public testing::Test {
 protected:
  void SetUp() override { ASSERT_TRUE(user_data_dir_.CreateUniqueTempDir()); }

  void TearDown() override { EXPECT_TRUE(user_data_dir_.Delete()); }

  base::FilePath user_data_dir() const { return user_data_dir_.GetPath(); }

  void CreateProfileDirectories() {
    for (const auto& path : kProfileDirectoryBaseNames) {
      ASSERT_TRUE(base::CreateDirectory(user_data_dir().Append(path)));
      absolute_profile_directories_.push_back(user_data_dir().Append(path));
    }
  }

  const std::vector<base::FilePath>& absolute_profile_directories() const {
    return absolute_profile_directories_;
  }

  base::FilePath GetSnapshotDirectory(const base::Version& version) const {
    return user_data_dir()
        .Append(kSnapshotsDir)
        .AppendASCII(version.GetString());
  }

 private:
  base::ScopedTempDir user_data_dir_;
  std::vector<base::FilePath> absolute_profile_directories_;
};

TEST_F(SnapshotManagerTest, TakeSnapshot) {
  base::Version version("10.0.0");

  ASSERT_NO_FATAL_FAILURE(CreateProfileDirectories());
  const auto& absolute_profile_paths = absolute_profile_directories();

  // Files and folders at User Data level that should not be snapshotted.
  ASSERT_NO_FATAL_FAILURE(
      TestFolderAndFiles::CreateFilesAndFolders(user_data_dir()));

  // Files and folders at User Data level that should be snapshotted.
  base::File user_data_file(user_data_dir().Append(kUserDataFile),
                            base::File::FLAG_CREATE | base::File::FLAG_WRITE);
  ASSERT_NO_FATAL_FAILURE(TestFolderAndFiles::CreateFilesAndFolders(
      user_data_dir().Append(kUserDataFolder)));

  for (const auto& path : absolute_profile_paths) {
    // Files and folders at Profile Data level that should be snapshotted.
    base::File file(path.Append(kProfileDataFile),
                    base::File::FLAG_CREATE | base::File::FLAG_WRITE);
    base::File file_ext(path.Append(kProfileDataExtFile),
                        base::File::FLAG_CREATE | base::File::FLAG_WRITE);
    base::File file_journal(path.Append(kProfileDataJournalFile),
                            base::File::FLAG_CREATE | base::File::FLAG_WRITE);
    base::File file_ext_wal(path.Append(kProfileDataExtWalFile),
                            base::File::FLAG_CREATE | base::File::FLAG_WRITE);
    base::File file_ext_shm(path.Append(kProfileDataExtShmFile),
                            base::File::FLAG_CREATE | base::File::FLAG_WRITE);

    ASSERT_NO_FATAL_FAILURE(TestFolderAndFiles::CreateFilesAndFolders(
        path.Append(kProfileDataFolder)));

    // Files and folders at Profile Data level that should not be snapshotted.
    ASSERT_NO_FATAL_FAILURE(TestFolderAndFiles::CreateFilesAndFolders(path));
  }

  TestDowngradeManagerDelegate delegate;
  SnapshotManager snapshot_manager(user_data_dir(), &delegate);
  snapshot_manager.TakeSnapshot(version);

  auto snapshot_dir = GetSnapshotDirectory(version);

  EXPECT_TRUE(base::PathExists(user_data_dir().Append(kUserDataFile)));
  EXPECT_TRUE(base::DirectoryExists(user_data_dir().Append(kUserDataFolder)));
  EXPECT_TRUE(base::PathExists(snapshot_dir.Append(kUserDataFile)));
  EXPECT_TRUE(base::DirectoryExists(snapshot_dir.Append(kUserDataFolder)));
  EXPECT_TRUE(TestFolderAndFiles::AllPathExists(user_data_dir()));
  EXPECT_TRUE(TestFolderAndFiles::NoPathExists(snapshot_dir));

  for (const auto& path : absolute_profile_paths) {
    EXPECT_TRUE(base::PathExists(path.Append(kProfileDataFile)));
    EXPECT_TRUE(base::PathExists(path.Append(kProfileDataExtFile)));
    EXPECT_TRUE(base::PathExists(path.Append(kProfileDataJournalFile)));
    EXPECT_TRUE(base::PathExists(path.Append(kProfileDataExtWalFile)));
    EXPECT_TRUE(base::PathExists(path.Append(kProfileDataExtShmFile)));
    EXPECT_TRUE(base::DirectoryExists(path.Append(kProfileDataFolder)));
    EXPECT_TRUE(TestFolderAndFiles::AllPathExists(path));
  }

  for (const auto& path : kProfileDirectoryBaseNames) {
    EXPECT_TRUE(
        base::PathExists(snapshot_dir.Append(path).Append(kProfileDataFile)));
    EXPECT_TRUE(base::PathExists(
        snapshot_dir.Append(path).Append(kProfileDataJournalFile)));
    EXPECT_TRUE(base::PathExists(
        snapshot_dir.Append(path).Append(kProfileDataExtFile)));
    EXPECT_TRUE(base::PathExists(
        snapshot_dir.Append(path).Append(kProfileDataExtWalFile)));
    EXPECT_TRUE(base::PathExists(
        snapshot_dir.Append(path).Append(kProfileDataExtShmFile)));
    EXPECT_TRUE(base::DirectoryExists(
        snapshot_dir.Append(path).Append(kProfileDataFolder)));
    EXPECT_TRUE(TestFolderAndFiles::NoPathExists(snapshot_dir.Append(path)));
    EXPECT_TRUE(TestFolderAndFiles::AllPathExists(
        snapshot_dir.Append(path).Append(kProfileDataFolder)));
  }
}

TEST_F(SnapshotManagerTest, RestoreSnapshotOlderVersionAvailable) {
  base::Version older_version("9.0.0");
  base::Version existing_version("10.0.0");

  auto older_snapshot_dir = GetSnapshotDirectory(older_version);
  auto existing_snapshot_dir = GetSnapshotDirectory(existing_version);

  ASSERT_TRUE(base::CreateDirectory(older_snapshot_dir));
  ASSERT_NO_FATAL_FAILURE(
      TestFolderAndFiles::CreateFilesAndFolders(existing_snapshot_dir));

  base::File(older_snapshot_dir.Append(kDowngradeLastVersionFile),
             base::File::FLAG_CREATE | base::File::FLAG_WRITE);
  base::File(existing_snapshot_dir.Append(kDowngradeLastVersionFile),
             base::File::FLAG_CREATE | base::File::FLAG_WRITE);

  for (const auto& profile : kProfileDirectoryBaseNames) {
    const base::FilePath profile_folder = existing_snapshot_dir.Append(profile);
    ASSERT_NO_FATAL_FAILURE(
        TestFolderAndFiles::CreateFilesAndFolders(profile_folder));
  }

  TestDowngradeManagerDelegate delegate;
  SnapshotManager snapshot_manager(user_data_dir(), &delegate);
  snapshot_manager.RestoreSnapshot(base::Version("11.0.0"));

  EXPECT_TRUE(TestFolderAndFiles::AllPathExists(user_data_dir()));
  EXPECT_TRUE(TestFolderAndFiles::AllPathExists(existing_snapshot_dir));

  for (const auto& profile : kProfileDirectoryBaseNames) {
    EXPECT_TRUE(
        TestFolderAndFiles::AllPathExists(user_data_dir().Append(profile)));
    EXPECT_TRUE(TestFolderAndFiles::AllPathExists(
        existing_snapshot_dir.Append(profile)));
  }
}

TEST_F(SnapshotManagerTest, RestoreSnapshotTargetVersionAvailable) {
  base::Version older_version("9.0.0");
  base::Version version("10.0.0");

  auto existing_snapshot_dir = GetSnapshotDirectory(version);
  auto older_snapshot_dir = GetSnapshotDirectory(older_version);

  ASSERT_TRUE(base::CreateDirectory(older_snapshot_dir));
  ASSERT_NO_FATAL_FAILURE(
      TestFolderAndFiles::CreateFilesAndFolders(existing_snapshot_dir));

  base::File(older_snapshot_dir.Append(kDowngradeLastVersionFile),
             base::File::FLAG_CREATE | base::File::FLAG_WRITE);
  base::File(existing_snapshot_dir.Append(kDowngradeLastVersionFile),
             base::File::FLAG_CREATE | base::File::FLAG_WRITE);

  for (const auto& profile : kProfileDirectoryBaseNames) {
    const base::FilePath profile_folder = existing_snapshot_dir.Append(profile);
    ASSERT_NO_FATAL_FAILURE(
        TestFolderAndFiles::CreateFilesAndFolders(profile_folder));
  }

  TestDowngradeManagerDelegate delegate;
  SnapshotManager snapshot_manager(user_data_dir(), &delegate);
  snapshot_manager.RestoreSnapshot(version);

  EXPECT_TRUE(TestFolderAndFiles::AllPathExists(user_data_dir()));
  EXPECT_TRUE(TestFolderAndFiles::NoPathExists(existing_snapshot_dir));
  EXPECT_TRUE(
      base::PathExists(user_data_dir().Append(kDowngradeLastVersionFile)));

  for (const auto& profile : kProfileDirectoryBaseNames) {
    EXPECT_TRUE(
        TestFolderAndFiles::AllPathExists(user_data_dir().Append(profile)));
    EXPECT_TRUE(TestFolderAndFiles::NoPathExists(
        existing_snapshot_dir.Append(profile)));
  }
}

TEST_F(SnapshotManagerTest, RestoreSnapshotIgnoresIncompleteSnapshots) {
  base::Version older_version("9.0.0");
  base::Version existing_version("10.0.0");
  base::Version newer_version("11.0.0");

  auto older_snapshot_dir = GetSnapshotDirectory(older_version);
  auto existing_snapshot_dir = GetSnapshotDirectory(existing_version);
  auto newer_snapshot_dir = GetSnapshotDirectory(newer_version);

  ASSERT_NO_FATAL_FAILURE(
      TestFolderAndFiles::CreateFilesAndFolders(older_snapshot_dir));
  ASSERT_NO_FATAL_FAILURE(
      TestFolderAndFiles::CreateFilesAndFolders(existing_snapshot_dir));
  ASSERT_NO_FATAL_FAILURE(
      TestFolderAndFiles::CreateFilesAndFolders(newer_snapshot_dir));

  // Only the older directory is a complete snapshot.
  base::File(older_snapshot_dir.Append(kDowngradeLastVersionFile),
             base::File::FLAG_CREATE | base::File::FLAG_WRITE);

  for (const auto& profile : kProfileDirectoryBaseNames) {
    const base::FilePath profile_folder = older_snapshot_dir.Append(profile);
    ASSERT_NO_FATAL_FAILURE(
        TestFolderAndFiles::CreateFilesAndFolders(profile_folder));
  }

  TestDowngradeManagerDelegate delegate;
  SnapshotManager snapshot_manager(user_data_dir(), &delegate);
  snapshot_manager.RestoreSnapshot(newer_version);

  EXPECT_TRUE(TestFolderAndFiles::AllPathExists(user_data_dir()));
  EXPECT_TRUE(TestFolderAndFiles::AllPathExists(older_snapshot_dir));
  EXPECT_TRUE(TestFolderAndFiles::AllPathExists(existing_snapshot_dir));
  EXPECT_TRUE(TestFolderAndFiles::AllPathExists(newer_snapshot_dir));
  EXPECT_TRUE(
      base::PathExists(user_data_dir().Append(kDowngradeLastVersionFile)));

  for (const auto& profile : kProfileDirectoryBaseNames) {
    EXPECT_TRUE(
        TestFolderAndFiles::AllPathExists(user_data_dir().Append(profile)));
    EXPECT_TRUE(
        TestFolderAndFiles::AllPathExists(older_snapshot_dir.Append(profile)));
  }
}

TEST_F(SnapshotManagerTest, PurgeInvalidAndOldSnapshotsKeepsMaxValidSnapshots) {
  std::vector<base::FilePath> invalid_snapshot_paths{
      user_data_dir().Append(kSnapshotsDir).AppendASCII("Bad format"),
      user_data_dir().Append(kSnapshotsDir).AppendASCII("10"),
  };
  for (const auto& path : invalid_snapshot_paths)
    ASSERT_TRUE(base::CreateDirectory(path));

  std::vector<base::FilePath> valid_snapshot_paths{
      user_data_dir().Append(kSnapshotsDir).AppendASCII("20"),
      user_data_dir().Append(kSnapshotsDir).AppendASCII("30"),
      user_data_dir().Append(kSnapshotsDir).AppendASCII("40"),
      user_data_dir().Append(kSnapshotsDir).AppendASCII("50"),
  };

  for (const auto& path : valid_snapshot_paths) {
    ASSERT_TRUE(base::CreateDirectory(path));
    base::File(path.Append(kDowngradeLastVersionFile),
               base::File::FLAG_CREATE | base::File::FLAG_WRITE);
  }

  int max_number_of_snapshots = 3;
  TestDowngradeManagerDelegate delegate;
  SnapshotManager snapshot_manager(user_data_dir(), &delegate);
  snapshot_manager.PurgeInvalidAndOldSnapshots(max_number_of_snapshots,
                                               std::nullopt);

  const base::FilePath deletion_directory =
      user_data_dir()
          .Append(kSnapshotsDir)
          .AddExtension(kDowngradeDeleteSuffix);

  // All invalid snapshots have been movec
  for (const auto& path : invalid_snapshot_paths) {
    EXPECT_FALSE(base::PathExists(path));
    EXPECT_TRUE(base::PathExists(deletion_directory.Append(path.BaseName())));
  }

  // Only 3 valid snapshots remains
  for (const base::FilePath& path : std::views::reverse(valid_snapshot_paths)) {
    EXPECT_EQ(base::PathExists(path), max_number_of_snapshots != 0);
    EXPECT_EQ(!base::PathExists(deletion_directory.Append(path.BaseName())),
              max_number_of_snapshots != 0);
    --max_number_of_snapshots;
  }
}

TEST_F(SnapshotManagerTest, PurgeInvalidAndOldSnapshotsKeepsValidSnapshots) {
  std::vector<base::FilePath> valid_snapshot_paths{
      user_data_dir().Append(kSnapshotsDir).AppendASCII("20"),
      user_data_dir().Append(kSnapshotsDir).AppendASCII("30"),
  };

  for (const auto& path : valid_snapshot_paths) {
    ASSERT_TRUE(base::CreateDirectory(path));
    base::File(path.Append(kDowngradeLastVersionFile),
               base::File::FLAG_CREATE | base::File::FLAG_WRITE);
  }

  int max_number_of_snapshots = 3;
  TestDowngradeManagerDelegate delegate;
  SnapshotManager snapshot_manager(user_data_dir(), &delegate);
  snapshot_manager.PurgeInvalidAndOldSnapshots(max_number_of_snapshots,
                                               std::nullopt);

  for (const auto& path : valid_snapshot_paths)
    EXPECT_TRUE(base::PathExists(path));
}

TEST_F(SnapshotManagerTest,
       PurgeInvalidAndOldSnapshotsKeepsValidSnapshotsPerMilestone) {
  std::vector<base::FilePath> valid_snapshot_paths{
      user_data_dir().Append(kSnapshotsDir).AppendASCII("19.0.0"),
      user_data_dir().Append(kSnapshotsDir).AppendASCII("20.0.0"),
      user_data_dir().Append(kSnapshotsDir).AppendASCII("20.0.1"),
      user_data_dir().Append(kSnapshotsDir).AppendASCII("21.0.1"),
  };

  for (const auto& path : valid_snapshot_paths) {
    ASSERT_TRUE(base::CreateDirectory(path));
    base::File(path.Append(kDowngradeLastVersionFile),
               base::File::FLAG_CREATE | base::File::FLAG_WRITE);
  }

  int max_number_of_snapshots = 1;
  TestDowngradeManagerDelegate delegate;
  SnapshotManager snapshot_manager(user_data_dir(), &delegate);
  snapshot_manager.PurgeInvalidAndOldSnapshots(max_number_of_snapshots, 20);

  EXPECT_TRUE(base::PathExists(valid_snapshot_paths[0]));
  EXPECT_FALSE(base::PathExists(valid_snapshot_paths[1]));
  EXPECT_TRUE(base::PathExists(valid_snapshot_paths[2]));
  EXPECT_TRUE(base::PathExists(valid_snapshot_paths[3]));
}

}  // namespace downgrade
