// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/dbus/fake_cros_disks_client.h"

#include <utility>

#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/single_thread_task_runner.h"
#include "base/task/post_task.h"
#include "base/threading/thread_task_runner_handle.h"
#include "url/gurl.h"

namespace chromeos {

namespace {

// Performs fake mounting by creating a directory with a dummy file.
MountError PerformFakeMount(const std::string& source_path,
                            const base::FilePath& mounted_path,
                            MountType type) {
  if (mounted_path.empty())
    return MOUNT_ERROR_INVALID_ARGUMENT;

  // Just create an empty directory and shows it as the mounted directory.
  if (!base::CreateDirectory(mounted_path)) {
    DLOG(ERROR) << "Failed to create directory at " << mounted_path.value();
    return MOUNT_ERROR_DIRECTORY_CREATION_FAILED;
  }

  // Fake network mounts are responsible for populating their mount paths so
  // don't need a dummy file.
  if (type == MOUNT_TYPE_NETWORK_STORAGE)
    return MOUNT_ERROR_NONE;

  // Put a dummy file.
  const base::FilePath dummy_file_path =
      mounted_path.Append("SUCCESSFULLY_PERFORMED_FAKE_MOUNT.txt");
  const std::string dummy_file_content = "This is a dummy file.";
  const int write_result = base::WriteFile(
      dummy_file_path, dummy_file_content.data(), dummy_file_content.size());
  if (write_result != static_cast<int>(dummy_file_content.size())) {
    DLOG(ERROR) << "Failed to put a dummy file at "
                << dummy_file_path.value();
    return MOUNT_ERROR_MOUNT_PROGRAM_FAILED;
  }

  return MOUNT_ERROR_NONE;
}

}  // namespace

FakeCrosDisksClient::FakeCrosDisksClient()
    : unmount_call_count_(0),
      unmount_error_(MOUNT_ERROR_NONE),
      format_call_count_(0),
      format_success_(true),
      rename_call_count_(0),
      rename_success_(true),
      weak_ptr_factory_(this) {}

FakeCrosDisksClient::~FakeCrosDisksClient() = default;

void FakeCrosDisksClient::Init(dbus::Bus* bus) {
}

void FakeCrosDisksClient::AddObserver(Observer* observer) {
  observer_list_.AddObserver(observer);
}

void FakeCrosDisksClient::RemoveObserver(Observer* observer) {
  observer_list_.RemoveObserver(observer);
}

void FakeCrosDisksClient::Mount(const std::string& source_path,
                                const std::string& source_format,
                                const std::string& mount_label,
                                const std::vector<std::string>& mount_options,
                                MountAccessMode access_mode,
                                RemountOption remount,
                                VoidDBusMethodCallback callback) {
  // This fake implementation assumes mounted path is device when source_format
  // is empty, or an archive otherwise.
  MountType type =
      source_format.empty() ? MOUNT_TYPE_DEVICE : MOUNT_TYPE_ARCHIVE;

  // Network storage source paths are URIs.
  if (GURL(source_path).is_valid())
    type = MOUNT_TYPE_NETWORK_STORAGE;

  base::FilePath mounted_path;
  switch (type) {
    case MOUNT_TYPE_ARCHIVE:
      mounted_path = GetArchiveMountPoint().Append(
          base::FilePath::FromUTF8Unsafe(mount_label));
      break;
    case MOUNT_TYPE_DEVICE:
      mounted_path = GetRemovableDiskMountPoint().Append(
          base::FilePath::FromUTF8Unsafe(mount_label));
      break;
    case MOUNT_TYPE_NETWORK_STORAGE:
      // Call all registered callbacks until mounted_path is non-empty.
      for (auto const& callback : custom_mount_point_callbacks_) {
        mounted_path = callback.Run(source_path, mount_options);
        if (!mounted_path.empty()) {
          break;
        }
      }
      break;
    case MOUNT_TYPE_INVALID:
      NOTREACHED();
      return;
  }
  mounted_paths_.insert(mounted_path);

  base::PostTaskWithTraitsAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
      base::BindOnce(&PerformFakeMount, source_path, mounted_path, type),
      base::BindOnce(&FakeCrosDisksClient::DidMount,
                     weak_ptr_factory_.GetWeakPtr(), source_path, type,
                     mounted_path, std::move(callback)));
}

void FakeCrosDisksClient::DidMount(const std::string& source_path,
                                   MountType type,
                                   const base::FilePath& mounted_path,
                                   VoidDBusMethodCallback callback,
                                   MountError mount_error) {
  // Tell the caller of Mount() that the mount request was accepted.
  // Note that even if PerformFakeMount fails, this calls with |true| to
  // emulate the situation that 1) Mount operation is _successfully_ started,
  // 2) then failed for some reason.
  std::move(callback).Run(true);

  // Notify observers that the mount is completed.
  NotifyMountCompleted(mount_error, source_path, type,
                       mounted_path.AsUTF8Unsafe());
}

void FakeCrosDisksClient::Unmount(const std::string& device_path,
                                  UnmountOptions options,
                                  UnmountCallback callback) {
  DCHECK(!callback.is_null());

  unmount_call_count_++;
  last_unmount_device_path_ = device_path;
  last_unmount_options_ = options;

  // Remove the dummy mounted directory if it exists.
  if (mounted_paths_.erase(base::FilePath::FromUTF8Unsafe(device_path))) {
    base::PostTaskWithTraitsAndReply(
        FROM_HERE,
        {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
         base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
        base::BindOnce(base::IgnoreResult(&base::DeleteFile),
                       base::FilePath::FromUTF8Unsafe(device_path),
                       true /* recursive */),
        base::BindOnce(std::move(callback), unmount_error_));
  } else {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), unmount_error_));
  }
  if (!unmount_listener_.is_null())
    unmount_listener_.Run();
}

void FakeCrosDisksClient::EnumerateDevices(EnumerateDevicesCallback callback,
                                           base::OnceClosure error_callback) {}

void FakeCrosDisksClient::EnumerateMountEntries(
    EnumerateMountEntriesCallback callback,
    base::OnceClosure error_callback) {}

void FakeCrosDisksClient::Format(const std::string& device_path,
                                 const std::string& filesystem,
                                 VoidDBusMethodCallback callback) {
  DCHECK(!callback.is_null());

  format_call_count_++;
  last_format_device_path_ = device_path;
  last_format_filesystem_ = filesystem;
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), format_success_));
}

void FakeCrosDisksClient::Rename(const std::string& device_path,
                                 const std::string& volume_name,
                                 VoidDBusMethodCallback callback) {
  DCHECK(!callback.is_null());

  rename_call_count_++;
  last_rename_device_path_ = device_path;
  last_rename_volume_name_ = volume_name;
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), rename_success_));
}

void FakeCrosDisksClient::GetDeviceProperties(
    const std::string& device_path,
    GetDevicePropertiesCallback callback,
    base::OnceClosure error_callback) {}

void FakeCrosDisksClient::NotifyMountCompleted(MountError error_code,
                                               const std::string& source_path,
                                               MountType mount_type,
                                               const std::string& mount_path) {
  for (auto& observer : observer_list_) {
    observer.OnMountCompleted(
        MountEntry(error_code, source_path, mount_type, mount_path));
  }
}

void FakeCrosDisksClient::NotifyFormatCompleted(
    FormatError error_code,
    const std::string& device_path) {
  for (auto& observer : observer_list_)
    observer.OnFormatCompleted(error_code, device_path);
}

void FakeCrosDisksClient::NotifyRenameCompleted(
    RenameError error_code,
    const std::string& device_path) {
  for (auto& observer : observer_list_)
    observer.OnRenameCompleted(error_code, device_path);
}

void FakeCrosDisksClient::AddCustomMountPointCallback(
    FakeCrosDisksClient::CustomMountPointCallback custom_mount_point_callback) {
  custom_mount_point_callbacks_.emplace_back(custom_mount_point_callback);
}

}  // namespace chromeos
