// Copyright 2013 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/sync/glue/extensions_activity_monitor.h"

#include <stdint.h>

#include <string>

#include "base/files/file_path.h"
#include "base/memory/raw_ref.h"
#include "base/path_service.h"
#include "base/values.h"
#include "chrome/browser/extensions/api/bookmarks/bookmarks_api.h"
#include "chrome/browser/extensions/api/bookmarks/bookmarks_api_watcher.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/base/testing_profile.h"
#include "components/sync/base/extensions_activity.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace browser_sync {

namespace {

using extensions::Extension;

using ::testing::Contains;
using ::testing::Key;

namespace keys = extensions::manifest_keys;

// Create and return an extension with the given path.
scoped_refptr<Extension> MakeExtension(const std::string& name) {
  base::FilePath path;
  EXPECT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &path));
  path = path.AppendASCII(name);

  base::DictValue value;
  value.Set(keys::kManifestVersion, 2);
  value.Set(keys::kVersion, "1.0.0.0");
  value.Set(keys::kName, name);
  std::u16string error;
  scoped_refptr<Extension> extension(Extension::Create(
      path, extensions::mojom::ManifestLocation::kInvalidLocation, value,
      Extension::NO_FLAGS, &error));
  EXPECT_TRUE(error.empty());
  return extension;
}

// Fire a bookmarks API event from the given extension the given
// number of times.
template <class T>
void FireBookmarksApiEvent(Profile* profile,
                           const scoped_refptr<Extension>& extension,
                           int repeats) {
  scoped_refptr<extensions::BookmarksFunction> bookmarks_function =
      base::MakeRefCounted<T>();
  bookmarks_function->set_extension(extension.get());
  bookmarks_function->set_histogram_value(T::static_histogram_value());
  bookmarks_function->SetName(T::static_function_name());
  // |bookmarks_function| won't be run, just passed to Notify(), so calling
  // ignore_did_respond_for_testing() is needed to avoid a DCHECK failure.
  bookmarks_function->ignore_did_respond_for_testing();
  for (int i = 0; i < repeats; i++) {
    extensions::BookmarksApiWatcher::GetForBrowserContext(profile)
        ->NotifyApiInvoked(bookmarks_function.get());
  }
}

class SyncChromeExtensionsActivityMonitorTest : public testing::Test {
 public:
  SyncChromeExtensionsActivityMonitorTest()
      : profile_(std::make_unique<TestingProfile>()),
        monitor_(profile_.get()),
        extension1_(MakeExtension("extension1")),
        extension2_(MakeExtension("extension2")),
        id1_(extension1_->id()),
        id2_(extension2_->id()) {}
  ~SyncChromeExtensionsActivityMonitorTest() override = default;

 private:
  content::BrowserTaskEnvironment task_environment_;

 protected:
  std::unique_ptr<TestingProfile> profile_;
  ExtensionsActivityMonitor monitor_;
  scoped_refptr<Extension> extension1_;
  scoped_refptr<Extension> extension2_;
  // IDs of |extension{1,2}_|.
  const raw_ref<const std::string> id1_;
  const raw_ref<const std::string> id2_;
};

// Fire some mutating bookmark API events with extension 1, then fire
// some mutating and non-mutating bookmark API events with extension
// 2.  Only the mutating events should be recorded by the
// syncer::ExtensionsActivityMonitor.
TEST_F(SyncChromeExtensionsActivityMonitorTest, Basic) {
  FireBookmarksApiEvent<extensions::BookmarksRemoveFunction>(profile_.get(),
                                                             extension1_, 1);
  FireBookmarksApiEvent<extensions::BookmarksMoveFunction>(profile_.get(),
                                                           extension1_, 1);
  FireBookmarksApiEvent<extensions::BookmarksUpdateFunction>(profile_.get(),
                                                             extension1_, 2);
  FireBookmarksApiEvent<extensions::BookmarksCreateFunction>(profile_.get(),
                                                             extension1_, 3);
  FireBookmarksApiEvent<extensions::BookmarksSearchFunction>(profile_.get(),
                                                             extension1_, 5);
  const uint32_t writes_by_extension1 = 1 + 1 + 2 + 3;

  FireBookmarksApiEvent<extensions::BookmarksRemoveTreeFunction>(
      profile_.get(), extension2_, 8);
  FireBookmarksApiEvent<extensions::BookmarksGetSubTreeFunction>(
      profile_.get(), extension2_, 13);
  FireBookmarksApiEvent<extensions::BookmarksGetChildrenFunction>(
      profile_.get(), extension2_, 21);
  FireBookmarksApiEvent<extensions::BookmarksGetTreeFunction>(profile_.get(),
                                                              extension2_, 33);
  const uint32_t writes_by_extension2 = 8;

  syncer::ExtensionsActivity::Records results;
  monitor_.GetExtensionsActivity()->GetAndClearRecords(&results);

  EXPECT_EQ(2U, results.size());
  EXPECT_THAT(results, Contains(Key(*id1_)));
  EXPECT_THAT(results, Contains(Key(*id2_)));
  EXPECT_EQ(writes_by_extension1, results[*id1_].bookmark_write_count);
  EXPECT_EQ(writes_by_extension2, results[*id2_].bookmark_write_count);
}

// Fire some mutating bookmark API events with both extensions.  Then
// get the records, fire some more mutating and non-mutating events,
// and put the old records back.  Those should be merged with the new
// records correctly.
TEST_F(SyncChromeExtensionsActivityMonitorTest, Put) {
  FireBookmarksApiEvent<extensions::BookmarksCreateFunction>(profile_.get(),
                                                             extension1_, 5);
  FireBookmarksApiEvent<extensions::BookmarksMoveFunction>(profile_.get(),
                                                           extension2_, 8);

  syncer::ExtensionsActivity::Records results;
  monitor_.GetExtensionsActivity()->GetAndClearRecords(&results);

  EXPECT_EQ(2U, results.size());
  EXPECT_EQ(5U, results[*id1_].bookmark_write_count);
  EXPECT_EQ(8U, results[*id2_].bookmark_write_count);

  FireBookmarksApiEvent<extensions::BookmarksGetTreeFunction>(profile_.get(),
                                                              extension2_, 3);
  FireBookmarksApiEvent<extensions::BookmarksUpdateFunction>(profile_.get(),
                                                             extension2_, 2);

  // Simulate a commit failure, which augments the active record set with the
  // refugee records.
  monitor_.GetExtensionsActivity()->PutRecords(results);
  syncer::ExtensionsActivity::Records new_records;
  monitor_.GetExtensionsActivity()->GetAndClearRecords(&new_records);

  EXPECT_EQ(2U, results.size());
  EXPECT_EQ(*id1_, new_records[*id1_].extension_id);
  EXPECT_EQ(*id2_, new_records[*id2_].extension_id);
  EXPECT_EQ(5U, new_records[*id1_].bookmark_write_count);
  EXPECT_EQ(8U + 2U, new_records[*id2_].bookmark_write_count);
}

// Fire some mutating bookmark API events and get the records multiple
// times.  The mintor should correctly clear its records every time
// they're returned.
TEST_F(SyncChromeExtensionsActivityMonitorTest, MultiGet) {
  FireBookmarksApiEvent<extensions::BookmarksCreateFunction>(profile_.get(),
                                                             extension1_, 5);

  syncer::ExtensionsActivity::Records results;
  monitor_.GetExtensionsActivity()->GetAndClearRecords(&results);

  EXPECT_EQ(1U, results.size());
  EXPECT_EQ(5U, results[*id1_].bookmark_write_count);

  monitor_.GetExtensionsActivity()->GetAndClearRecords(&results);
  EXPECT_TRUE(results.empty());

  FireBookmarksApiEvent<extensions::BookmarksCreateFunction>(profile_.get(),
                                                             extension1_, 3);
  monitor_.GetExtensionsActivity()->GetAndClearRecords(&results);

  EXPECT_EQ(1U, results.size());
  EXPECT_EQ(3U, results[*id1_].bookmark_write_count);
}

}  // namespace

}  // namespace browser_sync
