// Copyright 2011 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/installer/util/move_tree_work_item.h"

#include <windows.h>

#include <fstream>
#include <memory>

#include "base/base_paths.h"
#include "base/files/file_util.h"
#include "base/files/memory_mapped_file.h"
#include "base/files/scoped_temp_dir.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/installer/util/work_item.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {
class MoveTreeWorkItemTest : public testing::Test {
 protected:
  void SetUp() override {
    ASSERT_TRUE(temp_from_dir_.CreateUniqueTempDir());
    ASSERT_TRUE(temp_to_dir_.CreateUniqueTempDir());
  }

  base::ScopedTempDir temp_from_dir_;
  base::ScopedTempDir temp_to_dir_;
};

// Simple function to dump some text into a new file.
void CreateTextFile(const std::wstring& filename,
                    const std::wstring& contents) {
  std::wofstream file;
  file.open(base::WideToASCII(filename).c_str());
  ASSERT_TRUE(file.is_open());
  file << contents;
  file.close();
}

// Simple function to read text from a file.
std::wstring ReadTextFile(const base::FilePath& path) {
  WCHAR contents[64];
  std::wifstream file;
  file.open(base::WideToASCII(path.value()).c_str());
  EXPECT_TRUE(file.is_open());
  file.getline(contents, std::size(contents));
  file.close();
  return std::wstring(contents);
}

const wchar_t kTextContent1[] = L"Gooooooooooooooooooooogle";
const wchar_t kTextContent2[] = L"Overwrite Me";
}  // namespace

// Move one directory from source to destination when destination does not
// exist.
TEST_F(MoveTreeWorkItemTest, MoveDirectory) {
  // Create two level deep source dir
  base::FilePath from_dir1(temp_from_dir_.GetPath());
  from_dir1 = from_dir1.AppendASCII("From_Dir1");
  base::CreateDirectory(from_dir1);
  ASSERT_TRUE(base::PathExists(from_dir1));

  base::FilePath from_dir2(from_dir1);
  from_dir2 = from_dir2.AppendASCII("From_Dir2");
  base::CreateDirectory(from_dir2);
  ASSERT_TRUE(base::PathExists(from_dir2));

  base::FilePath from_file(from_dir2);
  from_file = from_file.AppendASCII("From_File");
  CreateTextFile(from_file.value(), kTextContent1);
  ASSERT_TRUE(base::PathExists(from_file));

  // Generate destination path
  base::FilePath to_dir(temp_from_dir_.GetPath());
  to_dir = to_dir.AppendASCII("To_Dir");
  ASSERT_FALSE(base::PathExists(to_dir));

  base::FilePath to_file(to_dir);
  to_file = to_file.AppendASCII("From_Dir2");
  to_file = to_file.AppendASCII("From_File");
  ASSERT_FALSE(base::PathExists(to_file));

  // test Do()
  std::unique_ptr<MoveTreeWorkItem> work_item(WorkItem::CreateMoveTreeWorkItem(
      from_dir1, to_dir, temp_to_dir_.GetPath(), WorkItem::MoveTreeOptions{}));
  EXPECT_TRUE(work_item->Do());

  EXPECT_FALSE(base::PathExists(from_dir1));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::PathExists(to_file));

  // test rollback()
  work_item->Rollback();

  EXPECT_TRUE(base::PathExists(from_dir1));
  EXPECT_TRUE(base::PathExists(from_file));
  EXPECT_FALSE(base::PathExists(to_dir));
}

// Move one directory from source to destination when destination already
// exists.
TEST_F(MoveTreeWorkItemTest, MoveDirectoryDestExists) {
  // Create two level deep source dir
  base::FilePath from_dir1(temp_from_dir_.GetPath());
  from_dir1 = from_dir1.AppendASCII("From_Dir1");
  base::CreateDirectory(from_dir1);
  ASSERT_TRUE(base::PathExists(from_dir1));

  base::FilePath from_dir2(from_dir1);
  from_dir2 = from_dir2.AppendASCII("From_Dir2");
  base::CreateDirectory(from_dir2);
  ASSERT_TRUE(base::PathExists(from_dir2));

  base::FilePath from_file(from_dir2);
  from_file = from_file.AppendASCII("From_File");
  CreateTextFile(from_file.value(), kTextContent1);
  ASSERT_TRUE(base::PathExists(from_file));

  // Create destination path
  base::FilePath to_dir(temp_from_dir_.GetPath());
  to_dir = to_dir.AppendASCII("To_Dir");
  base::CreateDirectory(to_dir);
  ASSERT_TRUE(base::PathExists(to_dir));

  base::FilePath orig_to_file(to_dir);
  orig_to_file = orig_to_file.AppendASCII("To_File");
  CreateTextFile(orig_to_file.value(), kTextContent2);
  ASSERT_TRUE(base::PathExists(orig_to_file));

  base::FilePath new_to_file(to_dir);
  new_to_file = new_to_file.AppendASCII("From_Dir2");
  new_to_file = new_to_file.AppendASCII("From_File");
  ASSERT_FALSE(base::PathExists(new_to_file));

  // test Do(), don't check for duplicates.
  std::unique_ptr<MoveTreeWorkItem> work_item(WorkItem::CreateMoveTreeWorkItem(
      from_dir1, to_dir, temp_to_dir_.GetPath(), WorkItem::MoveTreeOptions{}));
  EXPECT_TRUE(work_item->Do());

  EXPECT_FALSE(base::PathExists(from_dir1));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::PathExists(new_to_file));
  EXPECT_FALSE(base::PathExists(orig_to_file));

  // test rollback()
  work_item->Rollback();

  EXPECT_TRUE(base::PathExists(from_dir1));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_FALSE(base::PathExists(new_to_file));
  EXPECT_TRUE(base::PathExists(orig_to_file));
  EXPECT_EQ(0, ReadTextFile(orig_to_file).compare(kTextContent2));
  EXPECT_EQ(0, ReadTextFile(from_file).compare(kTextContent1));
}

// Move one file from source to destination when destination does not
// exist.
TEST_F(MoveTreeWorkItemTest, MoveAFile) {
  // Create a file inside source dir
  base::FilePath from_dir(temp_from_dir_.GetPath());
  from_dir = from_dir.AppendASCII("From_Dir");
  base::CreateDirectory(from_dir);
  ASSERT_TRUE(base::PathExists(from_dir));

  base::FilePath from_file(from_dir);
  from_file = from_file.AppendASCII("From_File");
  CreateTextFile(from_file.value(), kTextContent1);
  ASSERT_TRUE(base::PathExists(from_file));

  // Generate destination file name
  base::FilePath to_file(temp_from_dir_.GetPath());
  to_file = to_file.AppendASCII("To_File");
  ASSERT_FALSE(base::PathExists(to_file));

  // test Do()
  std::unique_ptr<MoveTreeWorkItem> work_item(WorkItem::CreateMoveTreeWorkItem(
      from_file, to_file, temp_to_dir_.GetPath(),
      WorkItem::WorkItem::MoveTreeOptions{}));
  EXPECT_TRUE(work_item->Do());

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_FALSE(base::PathExists(from_file));
  EXPECT_TRUE(base::PathExists(to_file));
  EXPECT_EQ(0, ReadTextFile(to_file).compare(kTextContent1));

  // test rollback()
  work_item->Rollback();

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_TRUE(base::PathExists(from_file));
  EXPECT_FALSE(base::PathExists(to_file));
  EXPECT_EQ(0, ReadTextFile(from_file).compare(kTextContent1));
}

// Move one file from source to destination when destination already
// exists.
TEST_F(MoveTreeWorkItemTest, MoveFileDestExists) {
  // Create a file inside source dir
  base::FilePath from_dir(temp_from_dir_.GetPath());
  from_dir = from_dir.AppendASCII("From_Dir");
  base::CreateDirectory(from_dir);
  ASSERT_TRUE(base::PathExists(from_dir));

  base::FilePath from_file(from_dir);
  from_file = from_file.AppendASCII("From_File");
  CreateTextFile(from_file.value(), kTextContent1);
  ASSERT_TRUE(base::PathExists(from_file));

  // Create destination path
  base::FilePath to_dir(temp_from_dir_.GetPath());
  to_dir = to_dir.AppendASCII("To_Dir");
  base::CreateDirectory(to_dir);
  ASSERT_TRUE(base::PathExists(to_dir));

  base::FilePath to_file(to_dir);
  to_file = to_file.AppendASCII("To_File");
  CreateTextFile(to_file.value(), kTextContent2);
  ASSERT_TRUE(base::PathExists(to_file));

  // test Do()
  std::unique_ptr<MoveTreeWorkItem> work_item(WorkItem::CreateMoveTreeWorkItem(
      from_file, to_dir, temp_to_dir_.GetPath(),
      WorkItem::WorkItem::MoveTreeOptions{}));
  EXPECT_TRUE(work_item->Do());

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_FALSE(base::PathExists(from_file));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_FALSE(base::PathExists(to_file));
  EXPECT_EQ(0, ReadTextFile(to_dir).compare(kTextContent1));

  // test rollback()
  work_item->Rollback();

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_EQ(0, ReadTextFile(from_file).compare(kTextContent1));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_EQ(0, ReadTextFile(to_file).compare(kTextContent2));
}

// Move one file from source to destination when destination already
// exists and is in use.
TEST_F(MoveTreeWorkItemTest, MoveFileDestInUse) {
  // Create a file inside source dir
  base::FilePath from_dir(temp_from_dir_.GetPath());
  from_dir = from_dir.AppendASCII("From_Dir");
  base::CreateDirectory(from_dir);
  ASSERT_TRUE(base::PathExists(from_dir));

  base::FilePath from_file(from_dir);
  from_file = from_file.AppendASCII("From_File");
  CreateTextFile(from_file.value(), kTextContent1);
  ASSERT_TRUE(base::PathExists(from_file));

  // Create an executable in destination path by copying ourself to it.
  base::FilePath to_dir(temp_from_dir_.GetPath());
  to_dir = to_dir.AppendASCII("To_Dir");
  base::CreateDirectory(to_dir);
  ASSERT_TRUE(base::PathExists(to_dir));

  wchar_t exe_full_path_str[MAX_PATH];
  ::GetModuleFileName(nullptr, exe_full_path_str, MAX_PATH);
  base::FilePath exe_full_path(exe_full_path_str);
  base::FilePath to_file(to_dir);
  to_file = to_file.AppendASCII("To_File");
  base::CopyFile(exe_full_path, to_file);
  ASSERT_TRUE(base::PathExists(to_file));

  // Run the executable in destination path
  STARTUPINFOW si = {sizeof(si)};
  PROCESS_INFORMATION pi = {0};
  ASSERT_TRUE(::CreateProcess(
      nullptr, const_cast<wchar_t*>(to_file.value().c_str()), nullptr, nullptr,
      FALSE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr, nullptr, &si, &pi));

  // test Do()
  std::unique_ptr<MoveTreeWorkItem> work_item(WorkItem::CreateMoveTreeWorkItem(
      from_file, to_file, temp_to_dir_.GetPath(),
      WorkItem::WorkItem::MoveTreeOptions{}));
  EXPECT_TRUE(work_item->Do());

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_FALSE(base::PathExists(from_file));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_EQ(0, ReadTextFile(to_file).compare(kTextContent1));

  // test rollback()
  work_item->Rollback();

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_EQ(0, ReadTextFile(from_file).compare(kTextContent1));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::ContentsEqual(exe_full_path, to_file));

  TerminateProcess(pi.hProcess, 0);
  EXPECT_TRUE(WaitForSingleObject(pi.hProcess, 10000) == WAIT_OBJECT_0);
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);
}

// Move one file that is in use to destination.
TEST_F(MoveTreeWorkItemTest, MoveFileInUse) {
  // Create an executable for source by copying ourself to a new source dir.
  base::FilePath from_dir(temp_from_dir_.GetPath());
  from_dir = from_dir.AppendASCII("From_Dir");
  base::CreateDirectory(from_dir);
  ASSERT_TRUE(base::PathExists(from_dir));

  wchar_t exe_full_path_str[MAX_PATH];
  ::GetModuleFileName(nullptr, exe_full_path_str, MAX_PATH);
  base::FilePath exe_full_path(exe_full_path_str);
  base::FilePath from_file(from_dir);
  from_file = from_file.AppendASCII("From_File");
  base::CopyFile(exe_full_path, from_file);
  ASSERT_TRUE(base::PathExists(from_file));

  // Create a destination source dir and generate destination file name.
  base::FilePath to_dir(temp_from_dir_.GetPath());
  to_dir = to_dir.AppendASCII("To_Dir");
  base::CreateDirectory(to_dir);
  ASSERT_TRUE(base::PathExists(to_dir));

  base::FilePath to_file(to_dir);
  to_file = to_file.AppendASCII("To_File");
  CreateTextFile(to_file.value(), kTextContent1);
  ASSERT_TRUE(base::PathExists(to_file));

  // Run the executable in source path
  STARTUPINFOW si = {sizeof(si)};
  PROCESS_INFORMATION pi = {0};
  ASSERT_TRUE(::CreateProcess(
      nullptr, const_cast<wchar_t*>(from_file.value().c_str()), nullptr,
      nullptr, FALSE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr, nullptr,
      &si, &pi));

  // test Do()
  std::unique_ptr<MoveTreeWorkItem> work_item(WorkItem::CreateMoveTreeWorkItem(
      from_file, to_file, temp_to_dir_.GetPath(),
      WorkItem::WorkItem::MoveTreeOptions{}));
  EXPECT_TRUE(work_item->Do());

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_FALSE(base::PathExists(from_file));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::ContentsEqual(exe_full_path, to_file));

  // Close the process and make sure all the conditions after Do() are
  // still true.
  TerminateProcess(pi.hProcess, 0);
  EXPECT_TRUE(WaitForSingleObject(pi.hProcess, 10000) == WAIT_OBJECT_0);
  CloseHandle(pi.hProcess);
  CloseHandle(pi.hThread);

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_FALSE(base::PathExists(from_file));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::ContentsEqual(exe_full_path, to_file));

  // test rollback()
  work_item->Rollback();

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_TRUE(base::ContentsEqual(exe_full_path, from_file));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_EQ(0, ReadTextFile(to_file).compare(kTextContent1));
}

// Move one file that cannot be deleted in strict mode.
TEST_F(MoveTreeWorkItemTest, MoveUndeletableFileStrict) {
  // Create a source file
  base::FilePath from_dir = temp_from_dir_.GetPath().AppendASCII("From_Dir");
  base::CreateDirectory(from_dir);
  ASSERT_TRUE(base::PathExists(from_dir));

  base::FilePath from_file = from_dir.AppendASCII("From_File");
  ASSERT_TRUE(base::WriteFile(from_file, base::as_byte_span(kTextContent1)));
  ASSERT_TRUE(base::PathExists(from_file));

  // Create a destination dir and generate destination file name.
  base::FilePath to_dir = temp_from_dir_.GetPath().AppendASCII("To_Dir");
  base::CreateDirectory(to_dir);
  ASSERT_TRUE(base::PathExists(to_dir));

  base::FilePath to_file = to_dir.AppendASCII("To_File");

  // Open the source file so that it cannot be deleted.
  base::File open_file(from_file,
                       base::File::FLAG_OPEN | base::File::FLAG_READ);

  // test Do()
  std::unique_ptr<MoveTreeWorkItem> work_item(WorkItem::CreateMoveTreeWorkItem(
      from_file, to_file, temp_to_dir_.GetPath(),
      WorkItem::WorkItem::MoveTreeOptions{}));
  EXPECT_FALSE(work_item->Do());

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_TRUE(base::PathExists(from_file));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::ContentsEqual(from_file, to_file));

  // Close the file and make sure all the conditions after Do() are still true.
  open_file.Close();

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_TRUE(base::PathExists(from_file));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::ContentsEqual(from_file, to_file));

  // test rollback()
  work_item->Rollback();

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_TRUE(base::PathExists(from_file));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_FALSE(base::PathExists(to_file));
}

// Move one file that cannot be deleted in lenient mode.
TEST_F(MoveTreeWorkItemTest, MoveUndeletableFileLenient) {
  // Create a source file
  base::FilePath from_dir = temp_from_dir_.GetPath().AppendASCII("From_Dir");
  base::CreateDirectory(from_dir);
  ASSERT_TRUE(base::PathExists(from_dir));

  base::FilePath from_file = from_dir.AppendASCII("From_File");
  ASSERT_TRUE(base::WriteFile(from_file, base::as_byte_span(kTextContent1)));
  ASSERT_TRUE(base::PathExists(from_file));

  // Create a destination dir and generate destination file name.
  base::FilePath to_dir = temp_from_dir_.GetPath().AppendASCII("To_Dir");
  base::CreateDirectory(to_dir);
  ASSERT_TRUE(base::PathExists(to_dir));

  base::FilePath to_file = to_dir.AppendASCII("To_File");

  // Open the source file so that it cannot be deleted.
  base::File open_file(from_file,
                       base::File::FLAG_OPEN | base::File::FLAG_READ);

  // test Do()
  std::unique_ptr<MoveTreeWorkItem> work_item(WorkItem::CreateMoveTreeWorkItem(
      from_file, to_file, temp_to_dir_.GetPath(),
      WorkItem::WorkItem::MoveTreeOptions{.lenient_deletion = true}));
  EXPECT_TRUE(work_item->Do());

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_TRUE(base::PathExists(from_file));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::ContentsEqual(from_file, to_file));

  // Close the file and make sure all the conditions after Do() are still true.
  open_file.Close();

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_TRUE(base::PathExists(from_file));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::ContentsEqual(from_file, to_file));

  // test rollback()
  work_item->Rollback();

  EXPECT_TRUE(base::PathExists(from_dir));
  EXPECT_TRUE(base::PathExists(from_file));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_FALSE(base::PathExists(to_file));
}

// Move one directory from source to destination when destination already
// exists.
TEST_F(MoveTreeWorkItemTest, MoveDirectoryDestExistsCheckForDuplicatesFull) {
  // Create two level deep source dir
  base::FilePath from_dir1(temp_from_dir_.GetPath());
  from_dir1 = from_dir1.AppendASCII("From_Dir1");
  base::CreateDirectory(from_dir1);
  ASSERT_TRUE(base::PathExists(from_dir1));

  base::FilePath from_dir2(from_dir1);
  from_dir2 = from_dir2.AppendASCII("From_Dir2");
  base::CreateDirectory(from_dir2);
  ASSERT_TRUE(base::PathExists(from_dir2));

  base::FilePath from_file(from_dir2);
  from_file = from_file.AppendASCII("From_File");
  CreateTextFile(from_file.value(), kTextContent1);
  ASSERT_TRUE(base::PathExists(from_file));

  // Create a file hierarchy identical to the one in the source directory.
  base::FilePath to_dir(temp_from_dir_.GetPath());
  to_dir = to_dir.AppendASCII("To_Dir");
  ASSERT_TRUE(base::CopyDirectory(from_dir1, to_dir, true));

  // Lock one of the files in the to destination directory to prevent moves.
  base::FilePath orig_to_file(
      to_dir.AppendASCII("From_Dir2").AppendASCII("From_File"));
  base::MemoryMappedFile mapped_file;
  EXPECT_TRUE(mapped_file.Initialize(orig_to_file));

  // First check that we can't do the regular Move().
  std::unique_ptr<MoveTreeWorkItem> work_item(WorkItem::CreateMoveTreeWorkItem(
      from_dir1, to_dir, temp_to_dir_.GetPath(),
      WorkItem::WorkItem::MoveTreeOptions{}));
  EXPECT_FALSE(work_item->Do());
  work_item->Rollback();

  // Now test Do() with the check for duplicates. This should pass.
  work_item.reset(WorkItem::CreateMoveTreeWorkItem(
      from_dir1, to_dir, temp_to_dir_.GetPath(),
      WorkItem::MoveTreeOptions{.check_for_duplicates = true}));
  EXPECT_TRUE(work_item->Do());

  // Make sure that we "moved" the files, i.e. that the source directory isn't
  // there anymore,
  EXPECT_FALSE(base::PathExists(from_dir1));
  // Make sure that the original directory structure and file are still present.
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::PathExists(orig_to_file));
  // Make sure that the backup path is not empty.
  EXPECT_FALSE(base::IsDirectoryEmpty(temp_to_dir_.GetPath()));

  // test rollback()
  work_item->Rollback();

  // Once we rollback all the original files should still be there, as should
  // the source files.
  EXPECT_TRUE(base::PathExists(from_dir1));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::PathExists(orig_to_file));
  EXPECT_EQ(0, ReadTextFile(orig_to_file).compare(kTextContent1));
  EXPECT_EQ(0, ReadTextFile(from_file).compare(kTextContent1));
}

// Move one directory from source to destination when destination already
// exists but contains only a subset of the files in source.
TEST_F(MoveTreeWorkItemTest, MoveDirectoryDestExistsCheckForDuplicatesPartial) {
  // Create two level deep source dir
  base::FilePath from_dir1(temp_from_dir_.GetPath());
  from_dir1 = from_dir1.AppendASCII("From_Dir1");
  base::CreateDirectory(from_dir1);
  ASSERT_TRUE(base::PathExists(from_dir1));

  base::FilePath from_dir2(from_dir1);
  from_dir2 = from_dir2.AppendASCII("From_Dir2");
  base::CreateDirectory(from_dir2);
  ASSERT_TRUE(base::PathExists(from_dir2));

  base::FilePath from_file(from_dir2);
  from_file = from_file.AppendASCII("From_File");
  CreateTextFile(from_file.value(), kTextContent1);
  ASSERT_TRUE(base::PathExists(from_file));

  base::FilePath from_file2(from_dir2);
  from_file2 = from_file2.AppendASCII("From_File2");
  CreateTextFile(from_file2.value(), kTextContent2);
  ASSERT_TRUE(base::PathExists(from_file2));

  // Create destination path
  base::FilePath to_dir(temp_from_dir_.GetPath());
  to_dir = to_dir.AppendASCII("To_Dir");
  base::CreateDirectory(to_dir);
  ASSERT_TRUE(base::PathExists(to_dir));

  // Create a sub-directory of the same name as in the source directory.
  base::FilePath to_dir2(to_dir);
  to_dir2 = to_dir2.AppendASCII("From_Dir2");
  base::CreateDirectory(to_dir2);
  ASSERT_TRUE(base::PathExists(to_dir2));

  // Create one of the files in the to sub-directory, but not the other.
  base::FilePath orig_to_file(to_dir2);
  orig_to_file = orig_to_file.AppendASCII("From_File");
  CreateTextFile(orig_to_file.value(), kTextContent1);
  ASSERT_TRUE(base::PathExists(orig_to_file));

  // test Do(), check for duplicates.
  std::unique_ptr<MoveTreeWorkItem> work_item(WorkItem::CreateMoveTreeWorkItem(
      from_dir1, to_dir, temp_to_dir_.GetPath(),
      WorkItem::MoveTreeOptions{.check_for_duplicates = true}));
  EXPECT_TRUE(work_item->Do());

  // Make sure that we "moved" the files, i.e. that the source directory isn't
  // there anymore,
  EXPECT_FALSE(base::PathExists(from_dir1));
  // Make sure that the original directory structure and file are still present.
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::PathExists(orig_to_file));
  // Make sure that the backup path is not empty.
  EXPECT_FALSE(base::IsDirectoryEmpty(temp_to_dir_.GetPath()));
  // Make sure that the "new" file is also present.
  base::FilePath new_to_file2(to_dir2);
  new_to_file2 = new_to_file2.AppendASCII("From_File2");
  EXPECT_TRUE(base::PathExists(new_to_file2));

  // test rollback()
  work_item->Rollback();

  // Once we rollback all the original files should still be there, as should
  // the source files.
  EXPECT_TRUE(base::PathExists(from_dir1));
  EXPECT_TRUE(base::PathExists(to_dir));
  EXPECT_TRUE(base::PathExists(orig_to_file));
  EXPECT_EQ(0, ReadTextFile(orig_to_file).compare(kTextContent1));
  EXPECT_EQ(0, ReadTextFile(from_file).compare(kTextContent1));

  // Also, after rollback the new "to" file should be gone.
  EXPECT_FALSE(base::PathExists(new_to_file2));
}

// Tests that rollback after a failed move leaves things as they were.
TEST_F(MoveTreeWorkItemTest, RollbackRecovers) {
  base::FilePath source_dir = temp_from_dir_.GetPath();
  base::FilePath dest_dir =
      temp_from_dir_.GetPath().Append(FILE_PATH_LITERAL("dest"));
  ASSERT_PRED1(base::CreateDirectory, dest_dir);
  base::FilePath temp_dir =
      temp_from_dir_.GetPath().Append(FILE_PATH_LITERAL("temp"));
  ASSERT_PRED1(base::CreateDirectory, temp_dir);

  // Put some files into the source and dest.
  base::FilePath alpha_basename = base::FilePath(FILE_PATH_LITERAL("alpha"));
  base::FilePath baker_basename = base::FilePath(FILE_PATH_LITERAL("baker"));
  base::FilePath charlie_basename =
      base::FilePath(FILE_PATH_LITERAL("charlie"));

  for (const auto& file : {alpha_basename, baker_basename, charlie_basename}) {
    ASSERT_TRUE(base::WriteFile(source_dir.Append(file),
                                base::as_byte_span(file.value())));
    ASSERT_TRUE(base::WriteFile(dest_dir.Append(file),
                                base::as_byte_span(file.value())));
  }

  // Map one dest file into memory so that it can't be moved into the backup
  // dir.
  base::MemoryMappedFile baker_mapped;
  ASSERT_TRUE(baker_mapped.Initialize(dest_dir.Append(baker_basename)));

  // Perform a move, expecting it to fail. Under the covers, the initial call to
  // ::MoveFileEx fails with ERROR_SHARING_VIOLATION. This triggers the
  // copy-and-delete flow, which will succeed to copy everything and then fail
  // during the delete phase on account of the in-use file.
  auto work_item = base::WrapUnique(WorkItem::CreateMoveTreeWorkItem(
      source_dir, dest_dir, temp_dir, WorkItem::WorkItem::MoveTreeOptions{}));
  ASSERT_FALSE(work_item->Do());

  // Expect that the mapped file (at least) remains in the dest.
  ASSERT_PRED1(base::PathExists, dest_dir.Append(baker_basename));

  // Rollback under the expectation that all files/directories deleted above
  // will be recovered from the backup.
  work_item->Rollback();

  // Verify that all files are back in the dest.
  for (const auto& file : {alpha_basename, baker_basename, charlie_basename}) {
    ASSERT_PRED1(base::PathExists, dest_dir.Append(file));
  }
}
