// Copyright (c) 2012 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 "base/bind.h"
#include "base/message_loop/message_loop.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/fake_cros_disks_client.h"
#include "chromeos/disks/disk_mount_manager.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using chromeos::disks::DiskMountManager;
using chromeos::CrosDisksClient;
using chromeos::DBusThreadManager;
using chromeos::FakeCrosDisksClient;
using testing::_;
using testing::Field;
using testing::InSequence;

namespace {

// Holds information needed to create a DiskMountManager::Disk instance.
struct TestDiskInfo {
  const char* source_path;
  const char* mount_path;
  const char* system_path;
  const char* file_path;
  const char* device_label;
  const char* drive_label;
  const char* vendor_id;
  const char* vendor_name;
  const char* product_id;
  const char* product_name;
  const char* fs_uuid;
  const char* system_path_prefix;
  chromeos::DeviceType device_type;
  uint64 size_in_bytes;
  bool is_parent;
  bool is_read_only;
  bool has_media;
  bool on_boot_device;
  bool on_removable_device;
  bool is_hidden;
};

// Holds information to create a DiskMOuntManager::MountPointInfo instance.
struct TestMountPointInfo {
  const char* source_path;
  const char* mount_path;
  chromeos::MountType mount_type;
  chromeos::disks::MountCondition mount_condition;
};

// List of disks held in DiskMountManager at the begining of the test.
const TestDiskInfo kTestDisks[] = {
  {
    "/device/source_path",
    "/device/mount_path",
    "/device/prefix/system_path",
    "/device/file_path",
    "/device/device_label",
    "/device/drive_label",
    "/device/vendor_id",
    "/device/vendor_name",
    "/device/product_id",
    "/device/product_name",
    "/device/fs_uuid",
    "/device/prefix",
    chromeos::DEVICE_TYPE_USB,
    1073741824,  // size in bytes
    false,  // is parent
    false,  // is read only
    true,  // has media
    false,  // is on boot device
    true,  // is on removable device
    false  // is hidden
  },
};

// List of mount points  held in DiskMountManager at the begining of the test.
const TestMountPointInfo kTestMountPoints[] = {
  {
    "/archive/source_path",
    "/archive/mount_path",
    chromeos::MOUNT_TYPE_ARCHIVE,
    chromeos::disks::MOUNT_CONDITION_NONE
  },
  {
    "/device/source_path",
    "/device/mount_path",
    chromeos::MOUNT_TYPE_DEVICE,
    chromeos::disks::MOUNT_CONDITION_NONE
  },
};

// Mocks DiskMountManager observer.
class MockDiskMountManagerObserver : public DiskMountManager::Observer {
 public:
  virtual ~MockDiskMountManagerObserver() {}

  MOCK_METHOD2(OnDiskEvent, void(DiskMountManager::DiskEvent event,
                                 const DiskMountManager::Disk* disk));
  MOCK_METHOD2(OnDeviceEvent, void(DiskMountManager::DeviceEvent event,
                                   const std::string& device_path));
  MOCK_METHOD3(OnMountEvent,
      void(DiskMountManager::MountEvent event,
           chromeos::MountError error_code,
           const DiskMountManager::MountPointInfo& mount_point));
  MOCK_METHOD3(OnFormatEvent,
      void(DiskMountManager::FormatEvent event,
           chromeos::FormatError error_code,
           const std::string& device_path));
};

class DiskMountManagerTest : public testing::Test {
 public:
  DiskMountManagerTest() {}
  virtual ~DiskMountManagerTest() {}

  // Sets up test dbus tread manager and disks mount manager.
  // Initializes disk mount manager disks and mount points.
  // Adds a test observer to the disk mount manager.
  virtual void SetUp() {
    fake_cros_disks_client_ = new FakeCrosDisksClient;
    DBusThreadManager::GetSetterForTesting()->SetCrosDisksClient(
        scoped_ptr<CrosDisksClient>(fake_cros_disks_client_));

    DiskMountManager::Initialize();

    InitDisksAndMountPoints();

    DiskMountManager::GetInstance()->AddObserver(&observer_);
  }

  // Shuts down dbus thread manager and disk moutn manager used in the test.
  virtual void TearDown() {
    DiskMountManager::GetInstance()->RemoveObserver(&observer_);
    DiskMountManager::Shutdown();
    DBusThreadManager::Shutdown();
  }

 protected:
  // Checks if disk mount manager contains a mount point with specified moutn
  // path.
  bool HasMountPoint(const std::string& mount_path) {
    const DiskMountManager::MountPointMap& mount_points =
        DiskMountManager::GetInstance()->mount_points();
    return mount_points.find(mount_path) != mount_points.end();
  }

 private:
  // Adds a new disk to the disk mount manager.
  void AddTestDisk(const TestDiskInfo& disk) {
    EXPECT_TRUE(DiskMountManager::GetInstance()->AddDiskForTest(
        new DiskMountManager::Disk(disk.source_path,
                                   disk.mount_path,
                                   disk.system_path,
                                   disk.file_path,
                                   disk.device_label,
                                   disk.drive_label,
                                   disk.vendor_id,
                                   disk.vendor_name,
                                   disk.product_id,
                                   disk.product_name,
                                   disk.fs_uuid,
                                   disk.system_path_prefix,
                                   disk.device_type,
                                   disk.size_in_bytes,
                                   disk.is_parent,
                                   disk.is_read_only,
                                   disk.has_media,
                                   disk.on_boot_device,
                                   disk.on_removable_device,
                                   disk.is_hidden)));
  }

  // Adds a new mount point to the disk mount manager.
  // If the moutn point is a device mount point, disk with its source path
  // should already be added to the disk mount manager.
  void AddTestMountPoint(const TestMountPointInfo& mount_point) {
    EXPECT_TRUE(DiskMountManager::GetInstance()->AddMountPointForTest(
        DiskMountManager::MountPointInfo(mount_point.source_path,
                                         mount_point.mount_path,
                                         mount_point.mount_type,
                                         mount_point.mount_condition)));
  }

  // Adds disks and mount points to disk mount manager.
  void InitDisksAndMountPoints() {
    // Disks should be  added first (when adding device mount points it is
    // expected that the corresponding disk is already added).
    for (size_t i = 0; i < arraysize(kTestDisks); i++)
      AddTestDisk(kTestDisks[i]);

    for (size_t i = 0; i < arraysize(kTestMountPoints); i++)
      AddTestMountPoint(kTestMountPoints[i]);
  }

 protected:
  chromeos::FakeCrosDisksClient* fake_cros_disks_client_;
  MockDiskMountManagerObserver observer_;
  base::MessageLoopForUI message_loop_;
};

// Tests that the observer gets notified on attempt to format non existent mount
// point.
TEST_F(DiskMountManagerTest, Format_NotMounted) {
  EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_COMPLETED,
                                       chromeos::FORMAT_ERROR_UNKNOWN,
                                       "/mount/non_existent"))
      .Times(1);
  DiskMountManager::GetInstance()->FormatMountedDevice("/mount/non_existent");
}

// Tests that it is not possible to format archive mount point.
TEST_F(DiskMountManagerTest, Format_Archive) {
  EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_COMPLETED,
                                       chromeos::FORMAT_ERROR_UNKNOWN,
                                       "/archive/source_path"))
      .Times(1);

  DiskMountManager::GetInstance()->FormatMountedDevice("/archive/mount_path");
}

// Tests that format fails if the device cannot be unmounted.
TEST_F(DiskMountManagerTest, Format_FailToUnmount) {
  // Before formatting mounted device, the device should be unmounted.
  // In this test unmount will fail, and there should be no attempt to
  // format the device.

  // Set up expectations for observer mock.
  // Observer should be notified that unmount attempt fails and format task
  // failed to start.
  {
    InSequence s;

    EXPECT_CALL(observer_,
        OnMountEvent(DiskMountManager::UNMOUNTING,
                     chromeos::MOUNT_ERROR_INTERNAL,
                     Field(&DiskMountManager::MountPointInfo::mount_path,
                           "/device/mount_path")))
        .Times(1);

    EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_COMPLETED,
                                         chromeos::FORMAT_ERROR_UNKNOWN,
                                         "/device/source_path"))
        .Times(1);
  }

  fake_cros_disks_client_->MakeUnmountFail();
  // Start test.
  DiskMountManager::GetInstance()->FormatMountedDevice("/device/mount_path");

  // Cros disks will respond asynchronoulsy, so let's drain the message loop.
  message_loop_.RunUntilIdle();

  EXPECT_EQ(1, fake_cros_disks_client_->unmount_call_count());
  EXPECT_EQ("/device/mount_path",
            fake_cros_disks_client_->last_unmount_device_path());
  EXPECT_EQ(chromeos::UNMOUNT_OPTIONS_NONE,
            fake_cros_disks_client_->last_unmount_options());
  EXPECT_EQ(0, fake_cros_disks_client_->format_call_count());

  // The device mount should still be here.
  EXPECT_TRUE(HasMountPoint("/device/mount_path"));
}

// Tests that observer is notified when cros disks fails to start format
// process.
TEST_F(DiskMountManagerTest, Format_FormatFailsToStart) {
  // Before formatting mounted device, the device should be unmounted.
  // In this test, unmount will succeed, but call to Format method will
  // fail.

  // Set up expectations for observer mock.
  // Observer should be notified that the device was unmounted and format task
  // failed to start.
  {
    InSequence s;

    EXPECT_CALL(observer_,
        OnMountEvent(DiskMountManager::UNMOUNTING,
                     chromeos::MOUNT_ERROR_NONE,
                     Field(&DiskMountManager::MountPointInfo::mount_path,
                           "/device/mount_path")))
        .Times(1);

    EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_COMPLETED,
                                         chromeos::FORMAT_ERROR_UNKNOWN,
                                         "/device/source_path"))
        .Times(1);
  }

  fake_cros_disks_client_->MakeFormatFail();
  // Start the test.
  DiskMountManager::GetInstance()->FormatMountedDevice("/device/mount_path");

  // Cros disks will respond asynchronoulsy, so let's drain the message loop.
  message_loop_.RunUntilIdle();

  EXPECT_EQ(1, fake_cros_disks_client_->unmount_call_count());
  EXPECT_EQ("/device/mount_path",
            fake_cros_disks_client_->last_unmount_device_path());
  EXPECT_EQ(chromeos::UNMOUNT_OPTIONS_NONE,
            fake_cros_disks_client_->last_unmount_options());
  EXPECT_EQ(1, fake_cros_disks_client_->format_call_count());
  EXPECT_EQ("/device/source_path",
            fake_cros_disks_client_->last_format_device_path());
  EXPECT_EQ("vfat", fake_cros_disks_client_->last_format_filesystem());

  // The device mount should be gone.
  EXPECT_FALSE(HasMountPoint("/device/mount_path"));
}

// Tests the case where there are two format requests for the same device.
TEST_F(DiskMountManagerTest, Format_ConcurrentFormatCalls) {
  // Only the first format request should be processed (the second unmount
  // request fails because the device is already unmounted at that point).
  // CrosDisksClient will report that the format process for the first request
  // is successfully started.

  // Set up expectations for observer mock.
  // The observer should get a FORMAT_STARTED event for one format request and a
  // FORMAT_COMPLETED with an error code for the other format request. The
  // formatting will be started only for the first request.
  // There should be only one UNMOUNTING event. The result of the second one
  // should not be reported as the mount point will go away after the first
  // request.
  //
  // Note that in this test the format completion signal will not be simulated,
  // so the observer should not get FORMAT_COMPLETED signal.
  {
    InSequence s;

    EXPECT_CALL(observer_,
        OnMountEvent(DiskMountManager::UNMOUNTING,
                     chromeos::MOUNT_ERROR_NONE,
                     Field(&DiskMountManager::MountPointInfo::mount_path,
                           "/device/mount_path")))
        .Times(1);

    EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_COMPLETED,
                                         chromeos::FORMAT_ERROR_UNKNOWN,
                                         "/device/source_path"))
        .Times(1);

    EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_STARTED,
                                         chromeos::FORMAT_ERROR_NONE,
                                         "/device/source_path"))
        .Times(1);
  }

  fake_cros_disks_client_->set_unmount_listener(
      base::Bind(&FakeCrosDisksClient::MakeUnmountFail,
                 base::Unretained(fake_cros_disks_client_)));
  // Start the test.
  DiskMountManager::GetInstance()->FormatMountedDevice("/device/mount_path");
  DiskMountManager::GetInstance()->FormatMountedDevice("/device/mount_path");

  // Cros disks will respond asynchronoulsy, so let's drain the message loop.
  message_loop_.RunUntilIdle();

  EXPECT_EQ(2, fake_cros_disks_client_->unmount_call_count());
  EXPECT_EQ("/device/mount_path",
            fake_cros_disks_client_->last_unmount_device_path());
  EXPECT_EQ(chromeos::UNMOUNT_OPTIONS_NONE,
            fake_cros_disks_client_->last_unmount_options());
  EXPECT_EQ(1, fake_cros_disks_client_->format_call_count());
  EXPECT_EQ("/device/source_path",
            fake_cros_disks_client_->last_format_device_path());
  EXPECT_EQ("vfat",
            fake_cros_disks_client_->last_format_filesystem());

  // The device mount should be gone.
  EXPECT_FALSE(HasMountPoint("/device/mount_path"));
}

// Tests the case when the format process actually starts and fails.
TEST_F(DiskMountManagerTest, Format_FormatFails) {
  // Both unmount and format device cals are successful in this test.

  // Set up expectations for observer mock.
  // The observer should get notified that the device was unmounted and that
  // formatting has started.
  // After the formatting starts, the test will simulate failing
  // FORMAT_COMPLETED signal, so the observer should also be notified the
  // formatting has failed (FORMAT_COMPLETED event).
  {
    InSequence s;

    EXPECT_CALL(observer_,
        OnMountEvent(DiskMountManager::UNMOUNTING,
                     chromeos::MOUNT_ERROR_NONE,
                     Field(&DiskMountManager::MountPointInfo::mount_path,
                           "/device/mount_path")))
        .Times(1);

    EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_STARTED,
                                         chromeos::FORMAT_ERROR_NONE,
                                         "/device/source_path"))
        .Times(1);

    EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_COMPLETED,
                                         chromeos::FORMAT_ERROR_UNKNOWN,
                                         "/device/source_path"))
        .Times(1);
  }

  // Start the test.
  DiskMountManager::GetInstance()->FormatMountedDevice("/device/mount_path");

  // Wait for Unmount and Format calls to end.
  message_loop_.RunUntilIdle();

  EXPECT_EQ(1, fake_cros_disks_client_->unmount_call_count());
  EXPECT_EQ("/device/mount_path",
            fake_cros_disks_client_->last_unmount_device_path());
  EXPECT_EQ(chromeos::UNMOUNT_OPTIONS_NONE,
            fake_cros_disks_client_->last_unmount_options());
  EXPECT_EQ(1, fake_cros_disks_client_->format_call_count());
  EXPECT_EQ("/device/source_path",
            fake_cros_disks_client_->last_format_device_path());
  EXPECT_EQ("vfat", fake_cros_disks_client_->last_format_filesystem());

  // The device should be unmounted by now.
  EXPECT_FALSE(HasMountPoint("/device/mount_path"));

  // Send failing FORMAT_COMPLETED signal.
  // The failure is marked by ! in fromt of the path (but this should change
  // soon).
  fake_cros_disks_client_->SendFormatCompletedEvent(
      chromeos::FORMAT_ERROR_UNKNOWN, "/device/source_path");
}

// Tests the case when formatting completes successfully.
TEST_F(DiskMountManagerTest, Format_FormatSuccess) {
  // Set up cros disks client mocks.
  // Both unmount and format device cals are successful in this test.

  // Set up expectations for observer mock.
  // The observer should receive UNMOUNTING, FORMAT_STARTED and FORMAT_COMPLETED
  // events (all of them without an error set).
  {
    InSequence s;

    EXPECT_CALL(observer_,
        OnMountEvent(DiskMountManager::UNMOUNTING,
                     chromeos::MOUNT_ERROR_NONE,
                     Field(&DiskMountManager::MountPointInfo::mount_path,
                           "/device/mount_path")))
        .Times(1);

    EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_STARTED,
                                         chromeos::FORMAT_ERROR_NONE,
                                         "/device/source_path"))
        .Times(1);

    EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_COMPLETED,
                                         chromeos::FORMAT_ERROR_NONE,
                                         "/device/source_path"))
        .Times(1);
  }

  // Start the test.
  DiskMountManager::GetInstance()->FormatMountedDevice("/device/mount_path");

  // Wait for Unmount and Format calls to end.
  message_loop_.RunUntilIdle();

  EXPECT_EQ(1, fake_cros_disks_client_->unmount_call_count());
  EXPECT_EQ("/device/mount_path",
            fake_cros_disks_client_->last_unmount_device_path());
  EXPECT_EQ(chromeos::UNMOUNT_OPTIONS_NONE,
            fake_cros_disks_client_->last_unmount_options());
  EXPECT_EQ(1, fake_cros_disks_client_->format_call_count());
  EXPECT_EQ("/device/source_path",
            fake_cros_disks_client_->last_format_device_path());
  EXPECT_EQ("vfat", fake_cros_disks_client_->last_format_filesystem());

  // The device should be unmounted by now.
  EXPECT_FALSE(HasMountPoint("/device/mount_path"));

  // Simulate cros_disks reporting success.
  fake_cros_disks_client_->SendFormatCompletedEvent(
      chromeos::FORMAT_ERROR_NONE, "/device/source_path");
}

// Tests that it's possible to format the device twice in a row (this may not be
// true if the list of pending formats is not properly cleared).
TEST_F(DiskMountManagerTest, Format_ConsecutiveFormatCalls) {
  // All unmount and format device cals are successful in this test.
  // Each of the should be made twice (once for each formatting task).

  // Set up expectations for observer mock.
  // The observer should receive UNMOUNTING, FORMAT_STARTED and FORMAT_COMPLETED
  // events (all of them without an error set) twice (once for each formatting
  // task).
  // Also, there should be a MOUNTING event when the device remounting is
  // simulated.
  EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_COMPLETED,
                                       chromeos::FORMAT_ERROR_NONE,
                                       "/device/source_path"))
      .Times(2);

  EXPECT_CALL(observer_, OnFormatEvent(DiskMountManager::FORMAT_STARTED,
                                       chromeos::FORMAT_ERROR_NONE,
                                       "/device/source_path"))
      .Times(2);

  EXPECT_CALL(observer_,
      OnMountEvent(DiskMountManager::UNMOUNTING,
                   chromeos::MOUNT_ERROR_NONE,
                   Field(&DiskMountManager::MountPointInfo::mount_path,
                         "/device/mount_path")))
      .Times(2);

  EXPECT_CALL(observer_,
      OnMountEvent(DiskMountManager::MOUNTING,
                   chromeos::MOUNT_ERROR_NONE,
                   Field(&DiskMountManager::MountPointInfo::mount_path,
                         "/device/mount_path")))
      .Times(1);

  // Start the test.
  DiskMountManager::GetInstance()->FormatMountedDevice("/device/mount_path");

  // Wait for Unmount and Format calls to end.
  message_loop_.RunUntilIdle();

  EXPECT_EQ(1, fake_cros_disks_client_->unmount_call_count());
  EXPECT_EQ("/device/mount_path",
            fake_cros_disks_client_->last_unmount_device_path());
  EXPECT_EQ(chromeos::UNMOUNT_OPTIONS_NONE,
            fake_cros_disks_client_->last_unmount_options());
  EXPECT_EQ(1, fake_cros_disks_client_->format_call_count());
  EXPECT_EQ("/device/source_path",
            fake_cros_disks_client_->last_format_device_path());
  EXPECT_EQ("vfat", fake_cros_disks_client_->last_format_filesystem());

  // The device should be unmounted by now.
  EXPECT_FALSE(HasMountPoint("/device/mount_path"));

  // Simulate cros_disks reporting success.
  fake_cros_disks_client_->SendFormatCompletedEvent(
      chromeos::FORMAT_ERROR_NONE, "/device/source_path");

  // Simulate the device remounting.
  fake_cros_disks_client_->SendMountCompletedEvent(
      chromeos::MOUNT_ERROR_NONE,
      "/device/source_path",
      chromeos::MOUNT_TYPE_DEVICE,
      "/device/mount_path");

  EXPECT_TRUE(HasMountPoint("/device/mount_path"));

  // Try formatting again.
  DiskMountManager::GetInstance()->FormatMountedDevice("/device/mount_path");

  // Wait for Unmount and Format calls to end.
  message_loop_.RunUntilIdle();

  EXPECT_EQ(2, fake_cros_disks_client_->unmount_call_count());
  EXPECT_EQ("/device/mount_path",
            fake_cros_disks_client_->last_unmount_device_path());
  EXPECT_EQ(chromeos::UNMOUNT_OPTIONS_NONE,
            fake_cros_disks_client_->last_unmount_options());
  EXPECT_EQ(2, fake_cros_disks_client_->format_call_count());
  EXPECT_EQ("/device/source_path",
            fake_cros_disks_client_->last_format_device_path());
  EXPECT_EQ("vfat", fake_cros_disks_client_->last_format_filesystem());

  // Simulate cros_disks reporting success.
  fake_cros_disks_client_->SendFormatCompletedEvent(
      chromeos::FORMAT_ERROR_NONE, "/device/source_path");
}

}  // namespace