// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/sync_user_events/user_event_sync_bridge.h"

#include <map>
#include <set>
#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
#include "components/sync/model/data_batch.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync/protocol/user_event_specifics.pb.h"
#include "components/sync/test/data_type_store_test_util.h"
#include "components/sync/test/mock_data_type_local_change_processor.h"
#include "google_apis/gaia/gaia_id.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace syncer {
namespace {

using sync_pb::UserEventSpecifics;
using testing::_;
using testing::ElementsAre;
using testing::InvokeWithoutArgs;
using testing::IsEmpty;
using testing::IsNull;
using testing::NotNull;
using testing::Pair;
using testing::Pointee;
using testing::Return;
using testing::SaveArg;
using testing::SizeIs;
using testing::UnorderedElementsAre;
using testing::WithArg;
using WriteBatch = DataTypeStore::WriteBatch;

MATCHER_P(MatchesUserEvent, expected, "") {
  if (!arg.has_user_event()) {
    *result_listener << "which is not a user event";
    return false;
  }
  const UserEventSpecifics& actual = arg.user_event();
  if (actual.event_time_usec() != expected.event_time_usec()) {
    return false;
  }
  if (actual.navigation_id() != expected.navigation_id()) {
    return false;
  }
  if (actual.session_id() != expected.session_id()) {
    return false;
  }
  return true;
}

UserEventSpecifics CreateSpecifics(int64_t event_time_usec,
                                   int64_t navigation_id,
                                   uint64_t session_id) {
  UserEventSpecifics specifics;
  specifics.set_event_time_usec(event_time_usec);
  specifics.set_navigation_id(navigation_id);
  specifics.set_session_id(session_id);
  return specifics;
}

std::unique_ptr<UserEventSpecifics> SpecificsUniquePtr(int64_t event_time_usec,
                                                       int64_t navigation_id,
                                                       uint64_t session_id) {
  return std::make_unique<UserEventSpecifics>(
      CreateSpecifics(event_time_usec, navigation_id, session_id));
}

class TestGlobalIdMapper : public GlobalIdMapper {
 public:
  void AddGlobalIdChangeObserver(GlobalIdChange callback) override {
    callback_ = std::move(callback);
  }

  int64_t GetLatestGlobalId(int64_t global_id) const override {
    auto iter = id_map_.find(global_id);
    return iter == id_map_.end() ? global_id : iter->second;
  }

  void ChangeId(int64_t old_id, int64_t new_id) {
    id_map_[old_id] = new_id;
    callback_.Run(old_id, new_id);
  }

 private:
  GlobalIdChange callback_;
  std::map<int64_t, int64_t> id_map_;
};

class UserEventSyncBridgeTest : public testing::Test {
 protected:
  UserEventSyncBridgeTest() { ResetBridge(); }

  void ResetBridge() {
    OnceDataTypeStoreFactory store_factory;
    if (bridge_) {
      // Carry over the underlying store from previous bridge instances.
      std::unique_ptr<DataTypeStore> store = bridge_->StealStoreForTest();
      bridge_.reset();
      store_factory =
          DataTypeStoreTestUtil::MoveStoreToFactory(std::move(store));
    } else {
      store_factory = DataTypeStoreTestUtil::FactoryForInMemoryStoreForTest();
    }
    bridge_ = std::make_unique<UserEventSyncBridge>(
        std::move(store_factory), mock_processor_.CreateForwardingProcessor(),
        &test_global_id_mapper_);
  }

  void WaitUntilModelReadyToSync(
      const GaiaId& gaia_id = GaiaId("test_account_id")) {
    base::RunLoop loop;
    base::RepeatingClosure quit_closure = loop.QuitClosure();
    // Let the bridge initialize fully, which should run ModelReadyToSync().
    ON_CALL(*processor(), ModelReadyToSync)
        .WillByDefault(InvokeWithoutArgs([=]() { quit_closure.Run(); }));
    loop.Run();
    ON_CALL(*processor(), IsTrackingMetadata()).WillByDefault(Return(true));
    ON_CALL(*processor(), TrackedGaiaId()).WillByDefault(Return(gaia_id));
  }

  static std::string GetStorageKey(const UserEventSpecifics& specifics) {
    return UserEventSyncBridge::GetStorageKeyFromSpecificsForTest(specifics);
  }

  UserEventSyncBridge* bridge() { return bridge_.get(); }
  MockDataTypeLocalChangeProcessor* processor() { return &mock_processor_; }
  TestGlobalIdMapper* mapper() { return &test_global_id_mapper_; }

  std::map<std::string, sync_pb::EntitySpecifics> GetAllDataForDebugging() {
    const std::unique_ptr<DataBatch> batch = bridge_->GetAllDataForDebugging();
    EXPECT_NE(nullptr, batch);

    std::map<std::string, sync_pb::EntitySpecifics> storage_key_to_specifics;
    if (batch != nullptr) {
      while (batch->HasNext()) {
        auto [key, data] = batch->Next();
        storage_key_to_specifics[key] = data->specifics;
      }
    }
    return storage_key_to_specifics;
  }

  std::unique_ptr<sync_pb::EntitySpecifics> GetDataForCommit(
      const std::string& storage_key) {
    const std::unique_ptr<DataBatch> batch =
        bridge_->GetDataForCommit({storage_key});
    EXPECT_NE(nullptr, batch);

    std::unique_ptr<sync_pb::EntitySpecifics> specifics;
    if (batch != nullptr && batch->HasNext()) {
      auto [key, data] = batch->Next();
      specifics = std::make_unique<sync_pb::EntitySpecifics>(data->specifics);
      EXPECT_FALSE(batch->HasNext());
    }
    return specifics;
  }

 private:
  base::test::TaskEnvironment task_environment_;
  testing::NiceMock<MockDataTypeLocalChangeProcessor> mock_processor_;
  TestGlobalIdMapper test_global_id_mapper_;
  std::unique_ptr<UserEventSyncBridge> bridge_;
};

TEST_F(UserEventSyncBridgeTest, MetadataIsInitialized) {
  EXPECT_CALL(*processor(), ModelReadyToSync(NotNull()));
  WaitUntilModelReadyToSync();
}

TEST_F(UserEventSyncBridgeTest, GetDataForCommit) {
  WaitUntilModelReadyToSync();
  const UserEventSpecifics specifics(CreateSpecifics(1u, 2u, 3u));
  std::string storage_key;
  EXPECT_CALL(*processor(), Put).WillOnce(WithArg<0>(SaveArg<0>(&storage_key)));
  bridge()->RecordUserEvent(std::make_unique<UserEventSpecifics>(specifics));

  // Existing specifics should be returned.
  EXPECT_THAT(GetDataForCommit(storage_key),
              Pointee(MatchesUserEvent(specifics)));
  // GetDataForCommit() should handle arbitrary storage key.
  EXPECT_THAT(GetDataForCommit("bogus"), IsNull());
}

TEST_F(UserEventSyncBridgeTest, SingleRecord) {
  WaitUntilModelReadyToSync();
  const UserEventSpecifics specifics(CreateSpecifics(1u, 2u, 3u));
  std::string storage_key;
  EXPECT_CALL(*processor(), Put).WillOnce(WithArg<0>(SaveArg<0>(&storage_key)));
  bridge()->RecordUserEvent(std::make_unique<UserEventSpecifics>(specifics));

  EXPECT_THAT(GetDataForCommit(storage_key),
              Pointee(MatchesUserEvent(specifics)));
  EXPECT_THAT(GetAllDataForDebugging(),
              ElementsAre(Pair(storage_key, MatchesUserEvent(specifics))));
}

TEST_F(UserEventSyncBridgeTest, ApplyDisableSyncChanges) {
  WaitUntilModelReadyToSync();
  const UserEventSpecifics specifics(CreateSpecifics(1u, 2u, 3u));
  bridge()->RecordUserEvent(std::make_unique<UserEventSpecifics>(specifics));
  ASSERT_THAT(GetAllDataForDebugging(), SizeIs(1));

  bridge()->ApplyDisableSyncChanges(WriteBatch::CreateMetadataChangeList());
  // The bridge may asynchronously query the store to choose what to delete.
  base::RunLoop().RunUntilIdle();

  EXPECT_THAT(GetAllDataForDebugging(), IsEmpty());
}

TEST_F(UserEventSyncBridgeTest, MultipleRecords) {
  WaitUntilModelReadyToSync();
  std::set<std::string> unique_storage_keys;
  EXPECT_CALL(*processor(), Put)
      .Times(4)
      .WillRepeatedly(
          [&unique_storage_keys](const std::string& storage_key,
                                 std::unique_ptr<EntityData> entity_data,
                                 MetadataChangeList* metadata_change_list) {
            unique_storage_keys.insert(storage_key);
          });

  bridge()->RecordUserEvent(SpecificsUniquePtr(1u, 1u, 1u));
  bridge()->RecordUserEvent(SpecificsUniquePtr(1u, 1u, 2u));
  bridge()->RecordUserEvent(SpecificsUniquePtr(1u, 2u, 2u));
  bridge()->RecordUserEvent(SpecificsUniquePtr(2u, 2u, 2u));

  EXPECT_EQ(2u, unique_storage_keys.size());
  EXPECT_THAT(GetAllDataForDebugging(), SizeIs(2));
}

TEST_F(UserEventSyncBridgeTest, ApplyIncrementalSyncChanges) {
  WaitUntilModelReadyToSync();
  std::string storage_key1;
  std::string storage_key2;
  EXPECT_CALL(*processor(), Put)
      .WillOnce(WithArg<0>(SaveArg<0>(&storage_key1)))
      .WillOnce(WithArg<0>(SaveArg<0>(&storage_key2)));

  bridge()->RecordUserEvent(SpecificsUniquePtr(1u, 1u, 1u));
  bridge()->RecordUserEvent(SpecificsUniquePtr(2u, 2u, 2u));
  EXPECT_THAT(GetAllDataForDebugging(), SizeIs(2));

  syncer::EntityChangeList entity_change_list;
  entity_change_list.push_back(
      EntityChange::CreateDelete(storage_key1, syncer::EntityData()));
  const auto error_on_delete = bridge()->ApplyIncrementalSyncChanges(
      bridge()->CreateMetadataChangeList(), std::move(entity_change_list));
  EXPECT_FALSE(error_on_delete);
  EXPECT_THAT(GetAllDataForDebugging(), SizeIs(1));
  EXPECT_THAT(GetDataForCommit(storage_key1), IsNull());
  EXPECT_THAT(GetDataForCommit(storage_key2), NotNull());
}

TEST_F(UserEventSyncBridgeTest, HandleGlobalIdChange) {
  WaitUntilModelReadyToSync();

  constexpr int64_t first_id = 11;
  constexpr int64_t second_id = 12;
  constexpr int64_t third_id = 13;
  constexpr int64_t fourth_id = 14;

  std::string storage_key;
  EXPECT_CALL(*processor(), Put).WillOnce(WithArg<0>(SaveArg<0>(&storage_key)));

  // This id update should be applied to the event as it is initially
  // recorded.
  mapper()->ChangeId(first_id, second_id);
  bridge()->RecordUserEvent(SpecificsUniquePtr(1u, first_id, 2u));
  EXPECT_THAT(GetAllDataForDebugging(),
              ElementsAre(Pair(storage_key, MatchesUserEvent(CreateSpecifics(
                                                1u, second_id, 2u)))));

  // This id update is done while the event is "in flight", and should result in
  // it being updated and re-sent to sync.
  EXPECT_CALL(*processor(), Put(storage_key, _, _));
  mapper()->ChangeId(second_id, third_id);
  EXPECT_THAT(GetAllDataForDebugging(),
              ElementsAre(Pair(storage_key, MatchesUserEvent(CreateSpecifics(
                                                1u, third_id, 2u)))));
  syncer::EntityChangeList entity_change_list;
  entity_change_list.push_back(
      EntityChange::CreateDelete(storage_key, syncer::EntityData()));
  const auto error_on_delete = bridge()->ApplyIncrementalSyncChanges(
      bridge()->CreateMetadataChangeList(), std::move(entity_change_list));
  EXPECT_FALSE(error_on_delete);
  EXPECT_THAT(GetAllDataForDebugging(), IsEmpty());

  // This id update should be ignored, since we received commit confirmation
  // above.
  EXPECT_CALL(*processor(), Put).Times(0);
  mapper()->ChangeId(third_id, fourth_id);
  EXPECT_THAT(GetAllDataForDebugging(), IsEmpty());
}

TEST_F(UserEventSyncBridgeTest, MulipleEventsChanging) {
  WaitUntilModelReadyToSync();

  constexpr int64_t first_id = 11;
  constexpr int64_t second_id = 12;
  constexpr int64_t third_id = 13;
  constexpr int64_t fourth_id = 14;
  const UserEventSpecifics specifics1 = CreateSpecifics(101u, first_id, 2u);
  const UserEventSpecifics specifics2 = CreateSpecifics(102u, second_id, 4u);
  const UserEventSpecifics specifics3 = CreateSpecifics(103u, third_id, 6u);
  const std::string key1 = GetStorageKey(specifics1);
  const std::string key2 = GetStorageKey(specifics2);
  const std::string key3 = GetStorageKey(specifics3);
  ASSERT_NE(key1, key2);
  ASSERT_NE(key1, key3);
  ASSERT_NE(key2, key3);

  bridge()->RecordUserEvent(std::make_unique<UserEventSpecifics>(specifics1));
  bridge()->RecordUserEvent(std::make_unique<UserEventSpecifics>(specifics2));
  bridge()->RecordUserEvent(std::make_unique<UserEventSpecifics>(specifics3));
  ASSERT_THAT(GetAllDataForDebugging(),
              UnorderedElementsAre(Pair(key1, MatchesUserEvent(specifics1)),
                                   Pair(key2, MatchesUserEvent(specifics2)),
                                   Pair(key3, MatchesUserEvent(specifics3))));

  mapper()->ChangeId(second_id, fourth_id);
  EXPECT_THAT(
      GetAllDataForDebugging(),
      UnorderedElementsAre(
          Pair(key1, MatchesUserEvent(specifics1)),
          Pair(key2, MatchesUserEvent(CreateSpecifics(102u, fourth_id, 4u))),
          Pair(key3, MatchesUserEvent(specifics3))));

  mapper()->ChangeId(first_id, fourth_id);
  mapper()->ChangeId(third_id, fourth_id);
  EXPECT_THAT(
      GetAllDataForDebugging(),
      UnorderedElementsAre(
          Pair(key1, MatchesUserEvent(CreateSpecifics(101u, fourth_id, 2u))),
          Pair(key2, MatchesUserEvent(CreateSpecifics(102u, fourth_id, 4u))),
          Pair(key3, MatchesUserEvent(CreateSpecifics(103u, fourth_id, 6u)))));
}

TEST_F(UserEventSyncBridgeTest, RecordBeforeMetadataLoads) {
  ON_CALL(*processor(), IsTrackingMetadata()).WillByDefault(Return(false));
  ON_CALL(*processor(), TrackedGaiaId()).WillByDefault(Return(GaiaId()));
  bridge()->RecordUserEvent(SpecificsUniquePtr(1u, 2u, 3u));
  EXPECT_CALL(*processor(), ModelReadyToSync);
  WaitUntilModelReadyToSync(GaiaId("gaia_id"));
  EXPECT_THAT(GetAllDataForDebugging(), IsEmpty());
}

}  // namespace

}  // namespace syncer
