// Copyright 2022 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/ash/file_manager/restore_io_task.h"

#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/i18n/time_formatting.h"
#include "base/rand_util.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/mock_callback.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/trash_unittest_base.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_url.h"
#include "storage/browser/test/test_file_system_context.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"

namespace file_manager::io_task {
namespace {

using ::base::test::RunClosure;
using ::testing::_;
using ::testing::Field;

class ScopedFileForTest {
 public:
  ScopedFileForTest(const base::FilePath& absolute_file_path,
                    const std::string& file_contents)
      : absolute_file_path_(absolute_file_path) {
    EXPECT_TRUE(base::WriteFile(absolute_file_path_, file_contents));
  }

  ~ScopedFileForTest() { EXPECT_TRUE(base::DeleteFile(absolute_file_path_)); }

  const base::FilePath GetPath() { return absolute_file_path_; }

 private:
  const base::FilePath absolute_file_path_;
};

class RestoreIOTaskTest : public TrashBaseIOTest {
 public:
  RestoreIOTaskTest() = default;

  RestoreIOTaskTest(const RestoreIOTaskTest&) = delete;
  RestoreIOTaskTest& operator=(const RestoreIOTaskTest&) = delete;

  std::string GenerateTrashInfoContents(const std::string& restore_path) {
    return base::StrCat({"[Trash Info]\nPath=", restore_path, "\nDeletionDate=",
                         base::TimeFormatAsIso8601(base::Time::UnixEpoch())});
  }

  void ExpectRestorePathFailure(const std::string& restore_path_in_trashinfo) {
    std::string foo_contents = base::RandBytesAsString(kTestFileSize);
    std::string foo_metadata_contents =
        GenerateTrashInfoContents(restore_path_in_trashinfo);

    const base::FilePath trash_path =
        downloads_dir_.Append(trash::kTrashFolderName);
    ScopedFileForTest trash_info_file(
        trash_path.Append(trash::kInfoFolderName).Append("foo.txt.trashinfo"),
        foo_metadata_contents);
    ScopedFileForTest trash_files_file(
        trash_path.Append(trash::kFilesFolderName).Append("foo.txt"),
        foo_contents);

    base::RunLoop run_loop;
    std::vector<storage::FileSystemURL> source_urls = {
        CreateFileSystemURL(trash_info_file.GetPath()),
    };

    base::MockRepeatingCallback<void(const ProgressStatus&)> progress_callback;
    base::MockOnceCallback<void(ProgressStatus)> complete_callback;

    EXPECT_CALL(progress_callback, Run(_)).Times(0);
    EXPECT_CALL(complete_callback,
                Run(Field(&ProgressStatus::state, State::kError)))
        .WillOnce(RunClosure(run_loop.QuitClosure()));

    RestoreIOTask task(source_urls, profile_.get(), file_system_context_,
                       temp_dir_.GetPath());
    task.Execute(progress_callback.Get(), complete_callback.Get());
    run_loop.Run();
  }

 private:
  content::BrowserTaskEnvironment task_environment_;
};

TEST_F(RestoreIOTaskTest, NoSourceUrlsShouldReturnSuccess) {
  base::RunLoop run_loop;
  std::vector<storage::FileSystemURL> source_urls;

  base::MockRepeatingCallback<void(const ProgressStatus&)> progress_callback;
  base::MockOnceCallback<void(ProgressStatus)> complete_callback;

  // We should get one complete callback when the size check of `source_urls`
  // finds none.
  EXPECT_CALL(complete_callback,
              Run(Field(&ProgressStatus::state, State::kSuccess)))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  RestoreIOTask task(source_urls, profile_.get(), file_system_context_,
                     temp_dir_.GetPath());
  task.Execute(progress_callback.Get(), complete_callback.Get());
  run_loop.Run();
}

TEST_F(RestoreIOTaskTest, URLsWithInvalidSuffixShouldError) {
  std::string foo_contents = base::RandBytesAsString(kTestFileSize);
  const base::FilePath file_path = temp_dir_.GetPath().Append("foo.txt");
  ASSERT_TRUE(base::WriteFile(file_path, foo_contents));

  base::RunLoop run_loop;
  std::vector<storage::FileSystemURL> source_urls = {
      CreateFileSystemURL(file_path),
  };

  base::MockRepeatingCallback<void(const ProgressStatus&)> progress_callback;
  base::MockOnceCallback<void(ProgressStatus)> complete_callback;

  // Progress callback should not be called as the suffix doesn't end in
  // .trashinfo.
  EXPECT_CALL(progress_callback, Run(_)).Times(0);

  // We should get one complete callback when the verification of the suffix
  // fails.
  EXPECT_CALL(complete_callback,
              Run(Field(&ProgressStatus::state, State::kError)))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  RestoreIOTask task(source_urls, profile_.get(), file_system_context_,
                     temp_dir_.GetPath());
  task.Execute(progress_callback.Get(), complete_callback.Get());
  run_loop.Run();
}

TEST_F(RestoreIOTaskTest, FilesNotInProperLocationShouldError) {
  std::string foo_contents = base::RandBytesAsString(kTestFileSize);
  const base::FilePath file_path =
      temp_dir_.GetPath().Append("foo.txt.trashinfo");
  ASSERT_TRUE(base::WriteFile(file_path, foo_contents));

  base::RunLoop run_loop;
  std::vector<storage::FileSystemURL> source_urls = {
      CreateFileSystemURL(file_path),
  };

  base::MockRepeatingCallback<void(const ProgressStatus&)> progress_callback;
  base::MockOnceCallback<void(ProgressStatus)> complete_callback;

  // Progress callback should not be called as the supplied file is not within
  // the .Trash/info directory.
  EXPECT_CALL(progress_callback, Run(_)).Times(0);

  // We should get one complete callback when the location is invalid.
  EXPECT_CALL(complete_callback,
              Run(Field(&ProgressStatus::state, State::kError)))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  RestoreIOTask task(source_urls, profile_.get(), file_system_context_,
                     temp_dir_.GetPath());
  task.Execute(progress_callback.Get(), complete_callback.Get());
  run_loop.Run();
}

TEST_F(RestoreIOTaskTest, MetadataWithNoCorrespondingFileShouldError) {
  EnsureTrashDirectorySetup(downloads_dir_);

  std::string foo_contents = base::RandBytesAsString(kTestFileSize);
  const base::FilePath file_path =
      downloads_dir_.Append(trash::kTrashFolderName)
          .Append(trash::kInfoFolderName)
          .Append("foo.txt.trashinfo");
  ASSERT_TRUE(base::WriteFile(file_path, foo_contents));

  base::RunLoop run_loop;
  std::vector<storage::FileSystemURL> source_urls = {
      CreateFileSystemURL(file_path),
  };

  base::MockRepeatingCallback<void(const ProgressStatus&)> progress_callback;
  base::MockOnceCallback<void(ProgressStatus)> complete_callback;

  // Progress callback should not be called as the corresponding file in the
  // .Trash/files location does not exist.
  EXPECT_CALL(progress_callback, Run(_)).Times(0);

  // We should get one complete callback when the .Trash/files path doesn't
  // exist.
  EXPECT_CALL(complete_callback,
              Run(Field(&ProgressStatus::state, State::kError)))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  RestoreIOTask task(source_urls, profile_.get(), file_system_context_,
                     temp_dir_.GetPath());
  task.Execute(progress_callback.Get(), complete_callback.Get());
  run_loop.Run();
}

TEST_F(RestoreIOTaskTest, InvalidRestorePaths) {
  EnsureTrashDirectorySetup(downloads_dir_);

  ExpectRestorePathFailure("/../../../bad/actor/foo.txt");
  ExpectRestorePathFailure("../../../bad/actor/foo.txt");
  ExpectRestorePathFailure("/");
}

TEST_F(RestoreIOTaskTest, ValidRestorePathShouldSucceedAndCreateDirectory) {
  EnsureTrashDirectorySetup(downloads_dir_);

  std::string foo_contents = base::RandBytesAsString(kTestFileSize);
  std::string foo_metadata_contents =
      GenerateTrashInfoContents("/Downloads/bar/foo.txt");

  const base::FilePath trash_path =
      downloads_dir_.Append(trash::kTrashFolderName);
  const base::FilePath info_file_path =
      trash_path.Append(trash::kInfoFolderName).Append("foo.txt.trashinfo");
  ASSERT_TRUE(base::WriteFile(info_file_path, foo_metadata_contents));
  const base::FilePath files_path =
      trash_path.Append(trash::kFilesFolderName).Append("foo.txt");
  ASSERT_TRUE(base::WriteFile(files_path, foo_contents));

  base::RunLoop run_loop;
  std::vector<storage::FileSystemURL> source_urls = {
      CreateFileSystemURL(info_file_path),
  };

  base::MockRepeatingCallback<void(const ProgressStatus&)> progress_callback;
  base::MockOnceCallback<void(ProgressStatus)> complete_callback;

  EXPECT_CALL(progress_callback, Run(_)).Times(0);
  EXPECT_CALL(complete_callback,
              Run(Field(&ProgressStatus::state, State::kSuccess)))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  RestoreIOTask task(source_urls, profile_.get(), file_system_context_,
                     temp_dir_.GetPath());
  task.Execute(progress_callback.Get(), complete_callback.Get());
  run_loop.Run();

  EXPECT_TRUE(base::PathExists(downloads_dir_.Append("bar").Append("foo.txt")));
}

TEST_F(RestoreIOTaskTest, ItemWithExistingConflictAreRenamed) {
  EnsureTrashDirectorySetup(downloads_dir_);

  std::string foo_contents = base::RandBytesAsString(kTestFileSize);
  std::string foo_metadata_contents =
      GenerateTrashInfoContents("/Downloads/bar/foo.txt");

  const base::FilePath trash_path =
      downloads_dir_.Append(trash::kTrashFolderName);
  const base::FilePath info_file_path =
      trash_path.Append(trash::kInfoFolderName).Append("foo.txt.trashinfo");
  ASSERT_TRUE(base::WriteFile(info_file_path, foo_metadata_contents));
  const base::FilePath files_path =
      trash_path.Append(trash::kFilesFolderName).Append("foo.txt");
  ASSERT_TRUE(base::WriteFile(files_path, foo_contents));

  // Create conflicting item at same place restore is going to happen at.
  const base::FilePath bar_dir = downloads_dir_.Append("bar");
  ASSERT_TRUE(base::CreateDirectory(bar_dir));
  ASSERT_TRUE(base::WriteFile(bar_dir.Append("foo.txt"), foo_contents));

  base::RunLoop run_loop;
  std::vector<storage::FileSystemURL> source_urls = {
      CreateFileSystemURL(info_file_path),
  };

  base::MockRepeatingCallback<void(const ProgressStatus&)> progress_callback;
  base::MockOnceCallback<void(ProgressStatus)> complete_callback;

  EXPECT_CALL(progress_callback, Run(_)).Times(0);
  EXPECT_CALL(complete_callback,
              Run(Field(&ProgressStatus::state, State::kSuccess)))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  RestoreIOTask task(source_urls, profile_.get(), file_system_context_,
                     temp_dir_.GetPath());
  task.Execute(progress_callback.Get(), complete_callback.Get());
  run_loop.Run();

  EXPECT_TRUE(base::PathExists(bar_dir.Append("foo.txt")));
  EXPECT_TRUE(base::PathExists(bar_dir.Append("foo (1).txt")));
  EXPECT_FALSE(base::PathExists(info_file_path));
}

class TrashServiceMaliciousPath
    : public ash::trash_service::mojom::TrashService {
 public:
  explicit TrashServiceMaliciousPath(
      mojo::PendingReceiver<ash::trash_service::mojom::TrashService> receiver) {
    receivers_.Add(this, std::move(receiver));
  }
  ~TrashServiceMaliciousPath() override = default;

  TrashServiceMaliciousPath(const TrashServiceMaliciousPath&) = delete;
  TrashServiceMaliciousPath& operator=(const TrashServiceMaliciousPath&) =
      delete;

  void ParseTrashInfoFile(
      base::File trash_info_file,
      ash::trash_service::ParseTrashInfoCallback callback) override {
    // Return a malicious path that traverses directories.
    std::move(callback).Run(base::File::FILE_OK,
                            base::FilePath("/../malicious/path"),
                            base::Time::UnixEpoch());
  }

 private:
  mojo::ReceiverSet<ash::trash_service::mojom::TrashService> receivers_;
};

class RestoreIOTaskMaliciousPathTest : public TrashBaseIOTest {
 public:
  RestoreIOTaskMaliciousPathTest() = default;

  RestoreIOTaskMaliciousPathTest(const RestoreIOTaskMaliciousPathTest&) =
      delete;
  RestoreIOTaskMaliciousPathTest& operator=(
      const RestoreIOTaskMaliciousPathTest&) = delete;

  void SetUp() override {
    TrashBaseIOTest::SetUp();

    ash::trash_service::SetTrashServiceLaunchOverrideForTesting(
        base::BindRepeating(
            &RestoreIOTaskMaliciousPathTest::CreateMaliciousTrashService,
            base::Unretained(this)));
  }

  mojo::PendingRemote<ash::trash_service::mojom::TrashService>
  CreateMaliciousTrashService() {
    mojo::PendingRemote<ash::trash_service::mojom::TrashService> remote;
    malicious_service_ = std::make_unique<TrashServiceMaliciousPath>(
        remote.InitWithNewPipeAndPassReceiver());
    return remote;
  }

 private:
  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<TrashServiceMaliciousPath> malicious_service_;
};

TEST_F(RestoreIOTaskMaliciousPathTest,
       CompromisedServiceShouldCompleteWithError) {
  EnsureTrashDirectorySetup(downloads_dir_);

  std::string foo_contents = base::RandBytesAsString(kTestFileSize);

  const base::FilePath trash_path =
      downloads_dir_.Append(trash::kTrashFolderName);
  const base::FilePath info_file_path =
      trash_path.Append(trash::kInfoFolderName).Append("foo.txt.trashinfo");
  ASSERT_TRUE(base::WriteFile(info_file_path, foo_contents));
  const base::FilePath files_path =
      trash_path.Append(trash::kFilesFolderName).Append("foo.txt");
  ASSERT_TRUE(base::WriteFile(files_path, foo_contents));

  base::RunLoop run_loop;
  std::vector<storage::FileSystemURL> source_urls = {
      CreateFileSystemURL(info_file_path),
  };

  base::MockRepeatingCallback<void(const ProgressStatus&)> progress_callback;
  base::MockOnceCallback<void(ProgressStatus)> complete_callback;

  EXPECT_CALL(progress_callback, Run(_)).Times(0);
  EXPECT_CALL(complete_callback,
              Run(Field(&ProgressStatus::state, State::kError)))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  RestoreIOTask task(source_urls, profile_.get(), file_system_context_,
                     temp_dir_.GetPath());
  task.Execute(progress_callback.Get(), complete_callback.Get());
  run_loop.Run();
}

class TrashServiceMojoDisconnector
    : public ash::trash_service::mojom::TrashService {
 public:
  explicit TrashServiceMojoDisconnector(
      mojo::PendingReceiver<ash::trash_service::mojom::TrashService> receiver) {
    receivers_.Add(this, std::move(receiver));
  }
  ~TrashServiceMojoDisconnector() override = default;

  TrashServiceMojoDisconnector(const TrashServiceMojoDisconnector&) = delete;
  TrashServiceMojoDisconnector& operator=(const TrashServiceMojoDisconnector&) =
      delete;

  // When the method is called, clear all mojo receivers. This is effectively
  // disconnecting the mojo pipe and emulates the disconnect handler being
  // invoked.
  void ParseTrashInfoFile(
      base::File trash_info_file,
      ash::trash_service::ParseTrashInfoCallback callback) override {
    receivers_.Clear();
  }

 private:
  mojo::ReceiverSet<ash::trash_service::mojom::TrashService> receivers_;
};

class RestoreIOTaskDisconnectMojoTest : public TrashBaseIOTest {
 public:
  RestoreIOTaskDisconnectMojoTest() = default;

  RestoreIOTaskDisconnectMojoTest(const RestoreIOTaskDisconnectMojoTest&) =
      delete;
  RestoreIOTaskDisconnectMojoTest& operator=(
      const RestoreIOTaskDisconnectMojoTest&) = delete;

 private:
  content::BrowserTaskEnvironment task_environment_;
};

TEST_F(RestoreIOTaskDisconnectMojoTest,
       TrashServiceMojoDisconnectShouldCompleteWithError) {
  EnsureTrashDirectorySetup(downloads_dir_);

  std::string foo_contents = base::RandBytesAsString(kTestFileSize);

  const base::FilePath trash_path =
      downloads_dir_.Append(trash::kTrashFolderName);
  const base::FilePath info_file_path =
      trash_path.Append(trash::kInfoFolderName).Append("foo.txt.trashinfo");
  ASSERT_TRUE(base::WriteFile(info_file_path, foo_contents));
  const base::FilePath files_path =
      trash_path.Append(trash::kFilesFolderName).Append("foo.txt");
  ASSERT_TRUE(base::WriteFile(files_path, foo_contents));

  base::RunLoop run_loop;
  std::vector<storage::FileSystemURL> source_urls = {
      CreateFileSystemURL(info_file_path),
  };

  base::MockRepeatingCallback<void(const ProgressStatus&)> progress_callback;
  base::MockOnceCallback<void(ProgressStatus)> complete_callback;

  EXPECT_CALL(progress_callback, Run(_)).Times(0);
  EXPECT_CALL(complete_callback,
              Run(Field(&ProgressStatus::state, State::kError)))
      .WillOnce(RunClosure(run_loop.QuitClosure()));

  RestoreIOTask task(source_urls, profile_.get(), file_system_context_,
                     temp_dir_.GetPath());
  task.Execute(progress_callback.Get(), complete_callback.Get());
  run_loop.Run();
}

}  // namespace
}  // namespace file_manager::io_task
