// 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 "content/public/test/test_file_error_injector.h"

#include <vector>

#include "base/compiler_specific.h"
#include "base/logging.h"
#include "content/browser/download/download_file_factory.h"
#include "content/browser/download/download_file_impl.h"
#include "content/browser/download/download_interrupt_reasons_impl.h"
#include "content/browser/download/download_manager_impl.h"
#include "content/browser/loader/resource_dispatcher_host_impl.h"
#include "content/public/browser/browser_thread.h"
#include "url/gurl.h"

namespace content {
class ByteStreamReader;

namespace {

// A class that performs file operations and injects errors.
class DownloadFileWithErrors: public DownloadFileImpl {
 public:
  typedef base::Callback<void(const GURL& url)> ConstructionCallback;
  typedef base::Callback<void(const GURL& url)> DestructionCallback;

  DownloadFileWithErrors(
      scoped_ptr<DownloadSaveInfo> save_info,
      const base::FilePath& default_download_directory,
      const GURL& url,
      const GURL& referrer_url,
      bool calculate_hash,
      scoped_ptr<ByteStreamReader> stream,
      const net::BoundNetLog& bound_net_log,
      base::WeakPtr<DownloadDestinationObserver> observer,
      const TestFileErrorInjector::FileErrorInfo& error_info,
      const ConstructionCallback& ctor_callback,
      const DestructionCallback& dtor_callback);

  virtual ~DownloadFileWithErrors();

  virtual void Initialize(const InitializeCallback& callback) OVERRIDE;

  // DownloadFile interface.
  virtual DownloadInterruptReason AppendDataToFile(
      const char* data, size_t data_len) OVERRIDE;
  virtual void RenameAndUniquify(
      const base::FilePath& full_path,
      const RenameCompletionCallback& callback) OVERRIDE;
  virtual void RenameAndAnnotate(
      const base::FilePath& full_path,
      const RenameCompletionCallback& callback) OVERRIDE;

 private:
  // Error generating helper.
  DownloadInterruptReason ShouldReturnError(
      TestFileErrorInjector::FileOperationCode code,
      DownloadInterruptReason original_error);

  // Determine whether to overwrite an operation with the given code
  // with a substitute error; if returns true, |*original_error| is
  // written with the error to use for overwriting.
  // NOTE: This routine changes state; specifically, it increases the
  // operations counts for the specified code.  It should only be called
  // once per operation.
  bool OverwriteError(
    TestFileErrorInjector::FileOperationCode code,
    DownloadInterruptReason* output_error);

  // Source URL for the file being downloaded.
  GURL source_url_;

  // Our injected error.  Only one per file.
  TestFileErrorInjector::FileErrorInfo error_info_;

  // Count per operation.  0-based.
  std::map<TestFileErrorInjector::FileOperationCode, int> operation_counter_;

  // Callback for destruction.
  DestructionCallback destruction_callback_;
};

static void InitializeErrorCallback(
    const DownloadFile::InitializeCallback original_callback,
    DownloadInterruptReason overwrite_error,
    DownloadInterruptReason original_error) {
  original_callback.Run(overwrite_error);
}

static void RenameErrorCallback(
    const DownloadFile::RenameCompletionCallback original_callback,
    DownloadInterruptReason overwrite_error,
    DownloadInterruptReason original_error,
    const base::FilePath& path_result) {
  original_callback.Run(
      overwrite_error,
      overwrite_error == DOWNLOAD_INTERRUPT_REASON_NONE ?
      path_result : base::FilePath());
}

DownloadFileWithErrors::DownloadFileWithErrors(
    scoped_ptr<DownloadSaveInfo> save_info,
    const base::FilePath& default_download_directory,
    const GURL& url,
    const GURL& referrer_url,
    bool calculate_hash,
    scoped_ptr<ByteStreamReader> stream,
    const net::BoundNetLog& bound_net_log,
    base::WeakPtr<DownloadDestinationObserver> observer,
    const TestFileErrorInjector::FileErrorInfo& error_info,
    const ConstructionCallback& ctor_callback,
    const DestructionCallback& dtor_callback)
        : DownloadFileImpl(
            save_info.Pass(), default_download_directory, url, referrer_url,
            calculate_hash, stream.Pass(), bound_net_log, observer),
          source_url_(url),
          error_info_(error_info),
          destruction_callback_(dtor_callback) {
  // DownloadFiles are created on the UI thread and are destroyed on the FILE
  // thread. Schedule the ConstructionCallback on the FILE thread so that if a
  // DownloadItem schedules a DownloadFile to be destroyed and creates another
  // one (as happens during download resumption), then the DestructionCallback
  // for the old DownloadFile is run before the ConstructionCallback for the
  // next DownloadFile.
  BrowserThread::PostTask(
      BrowserThread::FILE,
      FROM_HERE,
      base::Bind(ctor_callback, source_url_));
}

DownloadFileWithErrors::~DownloadFileWithErrors() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
  destruction_callback_.Run(source_url_);
}

void DownloadFileWithErrors::Initialize(
    const InitializeCallback& callback) {
  DownloadInterruptReason error_to_return = DOWNLOAD_INTERRUPT_REASON_NONE;
  InitializeCallback callback_to_use = callback;

  // Replace callback if the error needs to be overwritten.
  if (OverwriteError(
          TestFileErrorInjector::FILE_OPERATION_INITIALIZE,
          &error_to_return)) {
    if (DOWNLOAD_INTERRUPT_REASON_NONE != error_to_return) {
      // Don't execute a, probably successful, Initialize; just
      // return the error.
      BrowserThread::PostTask(
          BrowserThread::UI, FROM_HERE, base::Bind(
              callback, error_to_return));
      return;
    }

    // Otherwise, just wrap the return.
    callback_to_use = base::Bind(&InitializeErrorCallback, callback,
                                 error_to_return);
  }

  DownloadFileImpl::Initialize(callback_to_use);
}

DownloadInterruptReason DownloadFileWithErrors::AppendDataToFile(
    const char* data, size_t data_len) {
  return ShouldReturnError(
      TestFileErrorInjector::FILE_OPERATION_WRITE,
      DownloadFileImpl::AppendDataToFile(data, data_len));
}

void DownloadFileWithErrors::RenameAndUniquify(
    const base::FilePath& full_path,
    const RenameCompletionCallback& callback) {
  DownloadInterruptReason error_to_return = DOWNLOAD_INTERRUPT_REASON_NONE;
  RenameCompletionCallback callback_to_use = callback;

  // Replace callback if the error needs to be overwritten.
  if (OverwriteError(
          TestFileErrorInjector::FILE_OPERATION_RENAME_UNIQUIFY,
          &error_to_return)) {
    if (DOWNLOAD_INTERRUPT_REASON_NONE != error_to_return) {
      // Don't execute a, probably successful, RenameAndUniquify; just
      // return the error.
      BrowserThread::PostTask(
          BrowserThread::UI, FROM_HERE, base::Bind(
              callback, error_to_return, base::FilePath()));
      return;
    }

    // Otherwise, just wrap the return.
    callback_to_use = base::Bind(&RenameErrorCallback, callback,
                                 error_to_return);
  }

  DownloadFileImpl::RenameAndUniquify(full_path, callback_to_use);
}

void DownloadFileWithErrors::RenameAndAnnotate(
    const base::FilePath& full_path,
    const RenameCompletionCallback& callback) {
  DownloadInterruptReason error_to_return = DOWNLOAD_INTERRUPT_REASON_NONE;
  RenameCompletionCallback callback_to_use = callback;

  // Replace callback if the error needs to be overwritten.
  if (OverwriteError(
          TestFileErrorInjector::FILE_OPERATION_RENAME_ANNOTATE,
          &error_to_return)) {
    if (DOWNLOAD_INTERRUPT_REASON_NONE != error_to_return) {
      // Don't execute a, probably successful, RenameAndAnnotate; just
      // return the error.
      BrowserThread::PostTask(
          BrowserThread::UI, FROM_HERE, base::Bind(
              callback, error_to_return, base::FilePath()));
      return;
    }

    // Otherwise, just wrap the return.
    callback_to_use = base::Bind(&RenameErrorCallback, callback,
                                 error_to_return);
  }

  DownloadFileImpl::RenameAndAnnotate(full_path, callback_to_use);
}

bool DownloadFileWithErrors::OverwriteError(
    TestFileErrorInjector::FileOperationCode code,
    DownloadInterruptReason* output_error) {
  int counter = operation_counter_[code]++;

  if (code != error_info_.code)
    return false;

  if (counter != error_info_.operation_instance)
    return false;

  *output_error = error_info_.error;
  return true;
}

DownloadInterruptReason DownloadFileWithErrors::ShouldReturnError(
    TestFileErrorInjector::FileOperationCode code,
    DownloadInterruptReason original_error) {
  DownloadInterruptReason output_error = original_error;
  OverwriteError(code, &output_error);
  return output_error;
}

}  // namespace

// A factory for constructing DownloadFiles that inject errors.
class DownloadFileWithErrorsFactory : public DownloadFileFactory {
 public:
  DownloadFileWithErrorsFactory(
      const DownloadFileWithErrors::ConstructionCallback& ctor_callback,
      const DownloadFileWithErrors::DestructionCallback& dtor_callback);
  virtual ~DownloadFileWithErrorsFactory();

  // DownloadFileFactory interface.
  virtual DownloadFile* CreateFile(
      scoped_ptr<DownloadSaveInfo> save_info,
      const base::FilePath& default_download_directory,
      const GURL& url,
      const GURL& referrer_url,
      bool calculate_hash,
      scoped_ptr<ByteStreamReader> stream,
      const net::BoundNetLog& bound_net_log,
      base::WeakPtr<DownloadDestinationObserver> observer) OVERRIDE;

  bool AddError(
      const TestFileErrorInjector::FileErrorInfo& error_info);

  void ClearErrors();

 private:
  // Our injected error list, mapped by URL.  One per file.
   TestFileErrorInjector::ErrorMap injected_errors_;

  // Callback for creation and destruction.
  DownloadFileWithErrors::ConstructionCallback construction_callback_;
  DownloadFileWithErrors::DestructionCallback destruction_callback_;
};

DownloadFileWithErrorsFactory::DownloadFileWithErrorsFactory(
    const DownloadFileWithErrors::ConstructionCallback& ctor_callback,
    const DownloadFileWithErrors::DestructionCallback& dtor_callback)
        : construction_callback_(ctor_callback),
          destruction_callback_(dtor_callback) {
}

DownloadFileWithErrorsFactory::~DownloadFileWithErrorsFactory() {
}

DownloadFile* DownloadFileWithErrorsFactory::CreateFile(
    scoped_ptr<DownloadSaveInfo> save_info,
    const base::FilePath& default_download_directory,
    const GURL& url,
    const GURL& referrer_url,
    bool calculate_hash,
    scoped_ptr<ByteStreamReader> stream,
    const net::BoundNetLog& bound_net_log,
    base::WeakPtr<DownloadDestinationObserver> observer) {
  if (injected_errors_.find(url.spec()) == injected_errors_.end()) {
    // Have to create entry, because FileErrorInfo is not a POD type.
    TestFileErrorInjector::FileErrorInfo err_info = {
      url.spec(),
      TestFileErrorInjector::FILE_OPERATION_INITIALIZE,
      -1,
      DOWNLOAD_INTERRUPT_REASON_NONE
    };
    injected_errors_[url.spec()] = err_info;
  }

  return new DownloadFileWithErrors(
      save_info.Pass(),
      default_download_directory,
      url,
      referrer_url,
      calculate_hash,
      stream.Pass(),
      bound_net_log,
      observer,
      injected_errors_[url.spec()],
      construction_callback_,
      destruction_callback_);
}

bool DownloadFileWithErrorsFactory::AddError(
    const TestFileErrorInjector::FileErrorInfo& error_info) {
  // Creates an empty entry if necessary.  Duplicate entries overwrite.
  injected_errors_[error_info.url] = error_info;

  return true;
}

void DownloadFileWithErrorsFactory::ClearErrors() {
  injected_errors_.clear();
}

TestFileErrorInjector::TestFileErrorInjector(
    DownloadManager* download_manager)
    : created_factory_(NULL),
      // This code is only used for browser_tests, so a
      // DownloadManager is always a DownloadManagerImpl.
      download_manager_(static_cast<DownloadManagerImpl*>(download_manager)) {
  // Record the value of the pointer, for later validation.
  created_factory_ =
      new DownloadFileWithErrorsFactory(
          base::Bind(&TestFileErrorInjector::RecordDownloadFileConstruction,
                     this),
          base::Bind(&TestFileErrorInjector::RecordDownloadFileDestruction,
                     this));

  // We will transfer ownership of the factory to the download manager.
  scoped_ptr<DownloadFileFactory> download_file_factory(
      created_factory_);

  download_manager_->SetDownloadFileFactoryForTesting(
      download_file_factory.Pass());
}

TestFileErrorInjector::~TestFileErrorInjector() {
}

bool TestFileErrorInjector::AddError(const FileErrorInfo& error_info) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  DCHECK_LE(0, error_info.operation_instance);
  DCHECK(injected_errors_.find(error_info.url) == injected_errors_.end());

  // Creates an empty entry if necessary.
  injected_errors_[error_info.url] = error_info;

  return true;
}

void TestFileErrorInjector::ClearErrors() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  injected_errors_.clear();
}

bool TestFileErrorInjector::InjectErrors() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  ClearFoundFiles();

  DCHECK_EQ(static_cast<DownloadFileFactory*>(created_factory_),
            download_manager_->GetDownloadFileFactoryForTesting());

  created_factory_->ClearErrors();

  for (ErrorMap::const_iterator it = injected_errors_.begin();
       it != injected_errors_.end(); ++it)
    created_factory_->AddError(it->second);

  return true;
}

size_t TestFileErrorInjector::CurrentFileCount() const {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  return files_.size();
}

size_t TestFileErrorInjector::TotalFileCount() const {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  return found_files_.size();
}


bool TestFileErrorInjector::HadFile(const GURL& url) const {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  return (found_files_.find(url) != found_files_.end());
}

void TestFileErrorInjector::ClearFoundFiles() {
  found_files_.clear();
}

void TestFileErrorInjector::DownloadFileCreated(GURL url) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  DCHECK(files_.find(url) == files_.end());

  files_.insert(url);
  found_files_.insert(url);
}

void TestFileErrorInjector::DestroyingDownloadFile(GURL url) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  DCHECK(files_.find(url) != files_.end());

  files_.erase(url);
}

void TestFileErrorInjector::RecordDownloadFileConstruction(const GURL& url) {
  BrowserThread::PostTask(
      BrowserThread::UI,
      FROM_HERE,
      base::Bind(&TestFileErrorInjector::DownloadFileCreated, this, url));
}

void TestFileErrorInjector::RecordDownloadFileDestruction(const GURL& url) {
  BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
      base::Bind(&TestFileErrorInjector::DestroyingDownloadFile, this, url));
}

// static
scoped_refptr<TestFileErrorInjector> TestFileErrorInjector::Create(
    DownloadManager* download_manager) {
  static bool visited = false;
  DCHECK(!visited);  // Only allowed to be called once.
  visited = true;

  scoped_refptr<TestFileErrorInjector> single_injector(
      new TestFileErrorInjector(download_manager));

  return single_injector;
}

// static
std::string TestFileErrorInjector::DebugString(FileOperationCode code) {
  switch (code) {
    case FILE_OPERATION_INITIALIZE:
      return "INITIALIZE";
    case FILE_OPERATION_WRITE:
      return "WRITE";
    case FILE_OPERATION_RENAME_UNIQUIFY:
      return "RENAME_UNIQUIFY";
    case FILE_OPERATION_RENAME_ANNOTATE:
      return "RENAME_ANNOTATE";
    default:
      break;
  }

  return "Unknown";
}

}  // namespace content
