// Copyright 2018 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/engine/commit_contribution_impl.h"

#include <memory>
#include <string>
#include <utility>

#include "base/base64.h"
#include "base/functional/callback_helpers.h"
#include "base/hash/sha1.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "components/sync/base/client_tag_hash.h"
#include "components/sync/base/features.h"
#include "components/sync/base/model_type.h"
#include "components/sync/base/unique_position.h"
#include "components/sync/engine/nigori/cryptographer.h"
#include "components/sync/protocol/data_type_progress_marker.pb.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync/protocol/password_specifics.pb.h"
#include "components/sync/protocol/sharing_message_specifics.pb.h"
#include "components/sync/protocol/sync.pb.h"
#include "components/sync/protocol/sync_entity.pb.h"
#include "components/sync/test/fake_cryptographer.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace syncer {

namespace {

using sync_pb::CommitResponse;
using sync_pb::EntitySpecifics;
using sync_pb::SharingMessageCommitError;
using sync_pb::SyncEntity;

const ClientTagHash kTag = ClientTagHash::FromHashed("tag");
const char kValue[] = "value";
const char kURL[] = "url";
const char kTitle[] = "title";

EntitySpecifics GeneratePreferenceSpecifics(const ClientTagHash& tag,
                                            const std::string& value) {
  EntitySpecifics specifics;
  specifics.mutable_preference()->set_name(tag.value());
  specifics.mutable_preference()->set_value(value);
  return specifics;
}

EntitySpecifics GenerateBookmarkSpecifics(const std::string& url,
                                          const std::string& title) {
  EntitySpecifics specifics;
  specifics.mutable_bookmark()->set_legacy_canonicalized_title(title);
  if (url.empty()) {
    specifics.mutable_bookmark()->set_type(sync_pb::BookmarkSpecifics::FOLDER);
  } else {
    specifics.mutable_bookmark()->set_type(sync_pb::BookmarkSpecifics::URL);
    specifics.mutable_bookmark()->set_url(url);
  }
  *specifics.mutable_bookmark()->mutable_unique_position() =
      syncer::UniquePosition::FromInt64(10,
                                        syncer::UniquePosition::RandomSuffix())
          .ToProto();
  return specifics;
}

TEST(CommitContributionImplTest, PopulateCommitProtoDefault) {
  const int64_t kBaseVersion = 7;
  base::Time creation_time = base::Time::UnixEpoch() + base::Days(1);
  base::Time modification_time = creation_time + base::Seconds(1);

  auto data = std::make_unique<syncer::EntityData>();

  data->client_tag_hash = kTag;
  data->specifics = GeneratePreferenceSpecifics(kTag, kValue);

  // These fields are not really used for much, but we set them anyway
  // to make this item look more realistic.
  data->creation_time = creation_time;
  data->modification_time = modification_time;
  data->name = "Name:";

  CommitRequestData request_data;
  request_data.sequence_number = 2;
  request_data.base_version = kBaseVersion;
  base::Base64Encode(base::SHA1HashString(data->specifics.SerializeAsString()),
                     &request_data.specifics_hash);
  request_data.entity = std::move(data);

  SyncEntity entity;
  CommitContributionImpl::PopulateCommitProto(PREFERENCES, request_data,
                                              &entity);

  // Exhaustively verify the populated SyncEntity.
  EXPECT_TRUE(entity.id_string().empty());
  EXPECT_EQ(7, entity.version());
  EXPECT_EQ(modification_time.ToJsTime(), entity.mtime());
  EXPECT_EQ(creation_time.ToJsTime(), entity.ctime());
  EXPECT_FALSE(entity.name().empty());
  EXPECT_FALSE(entity.client_tag_hash().empty());
  EXPECT_EQ(kTag.value(), entity.specifics().preference().name());
  EXPECT_FALSE(entity.deleted());
  EXPECT_EQ(kValue, entity.specifics().preference().value());
  EXPECT_TRUE(entity.parent_id_string().empty());
  EXPECT_FALSE(entity.unique_position().has_custom_compressed_v1());
}

TEST(CommitContributionImplTest, PopulateCommitProtoBookmark) {
  const int64_t kBaseVersion = 7;
  base::Time creation_time = base::Time::UnixEpoch() + base::Days(1);
  base::Time modification_time = creation_time + base::Seconds(1);

  auto data = std::make_unique<syncer::EntityData>();

  data->id = "bookmark";
  data->specifics = GenerateBookmarkSpecifics(kURL, kTitle);

  // These fields are not really used for much, but we set them anyway
  // to make this item look more realistic.
  data->creation_time = creation_time;
  data->modification_time = modification_time;
  data->name = "Name:";
  data->legacy_parent_id = "ParentOf:";

  CommitRequestData request_data;
  request_data.sequence_number = 2;
  request_data.base_version = kBaseVersion;
  base::Base64Encode(base::SHA1HashString(data->specifics.SerializeAsString()),
                     &request_data.specifics_hash);
  request_data.entity = std::move(data);

  SyncEntity entity;
  CommitContributionImpl::PopulateCommitProto(BOOKMARKS, request_data, &entity);

  // Exhaustively verify the populated SyncEntity.
  EXPECT_FALSE(entity.id_string().empty());
  EXPECT_EQ(7, entity.version());
  EXPECT_EQ(modification_time.ToJsTime(), entity.mtime());
  EXPECT_EQ(creation_time.ToJsTime(), entity.ctime());
  EXPECT_FALSE(entity.name().empty());
  EXPECT_TRUE(entity.client_tag_hash().empty());
  EXPECT_EQ(kURL, entity.specifics().bookmark().url());
  EXPECT_FALSE(entity.deleted());
  EXPECT_EQ(kTitle, entity.specifics().bookmark().legacy_canonicalized_title());
  EXPECT_FALSE(entity.folder());
  EXPECT_FALSE(entity.parent_id_string().empty());
  EXPECT_TRUE(entity.unique_position().has_custom_compressed_v1());
}

TEST(CommitContributionImplTest, PopulateCommitProtoBookmarkFolder) {
  const int64_t kBaseVersion = 7;
  base::Time creation_time = base::Time::UnixEpoch() + base::Days(1);
  base::Time modification_time = creation_time + base::Seconds(1);

  auto data = std::make_unique<syncer::EntityData>();

  data->id = "bookmark";
  data->specifics = GenerateBookmarkSpecifics(/*url=*/"", kTitle);

  // These fields are not really used for much, but we set them anyway
  // to make this item look more realistic.
  data->creation_time = creation_time;
  data->modification_time = modification_time;
  data->name = "Name:";
  data->legacy_parent_id = "ParentOf:";

  CommitRequestData request_data;
  request_data.sequence_number = 2;
  request_data.base_version = kBaseVersion;
  base::Base64Encode(base::SHA1HashString(data->specifics.SerializeAsString()),
                     &request_data.specifics_hash);
  request_data.entity = std::move(data);

  SyncEntity entity;
  CommitContributionImpl::PopulateCommitProto(BOOKMARKS, request_data, &entity);

  // Exhaustively verify the populated SyncEntity.
  EXPECT_FALSE(entity.id_string().empty());
  EXPECT_EQ(7, entity.version());
  EXPECT_EQ(modification_time.ToJsTime(), entity.mtime());
  EXPECT_EQ(creation_time.ToJsTime(), entity.ctime());
  EXPECT_FALSE(entity.name().empty());
  EXPECT_TRUE(entity.client_tag_hash().empty());
  EXPECT_FALSE(entity.specifics().bookmark().has_url());
  EXPECT_FALSE(entity.deleted());
  EXPECT_EQ(kTitle, entity.specifics().bookmark().legacy_canonicalized_title());
  EXPECT_TRUE(entity.folder());
  EXPECT_FALSE(entity.parent_id_string().empty());
  EXPECT_TRUE(entity.unique_position().has_custom_compressed_v1());
}

// Verifies how PASSWORDS protos are committed on the wire, making sure the data
// is properly encrypted except for password metadata.
TEST(CommitContributionImplTest,
     PopulateCommitProtoPasswordWithoutCustomPassphrase) {
  const std::string kSignonRealm = "signon_realm";
  const int64_t kBaseVersion = 7;
  const int kDummyTimestamp = 123;

  auto data = std::make_unique<syncer::EntityData>();
  data->client_tag_hash = kTag;
  sync_pb::PasswordSpecificsData* password_data =
      data->specifics.mutable_password()->mutable_client_only_encrypted_data();
  password_data->set_signon_realm(kSignonRealm);
  password_data->set_date_last_used(kDummyTimestamp);

  data->specifics.mutable_password()->mutable_unencrypted_metadata()->set_url(
      kSignonRealm);
  data->specifics.mutable_password()
      ->mutable_unencrypted_metadata()
      ->set_blacklisted(false);
  data->specifics.mutable_password()
      ->mutable_unencrypted_metadata()
      ->set_date_last_used_windows_epoch_micros(kDummyTimestamp);

  auto request_data = std::make_unique<CommitRequestData>();
  request_data->sequence_number = 2;
  request_data->base_version = kBaseVersion;
  base::Base64Encode(base::SHA1HashString(data->specifics.SerializeAsString()),
                     &request_data->specifics_hash);
  request_data->entity = std::move(data);

  std::unique_ptr<FakeCryptographer> cryptographer =
      FakeCryptographer::FromSingleDefaultKey("dummy");

  CommitRequestDataList requests_data;
  requests_data.push_back(std::move(request_data));
  CommitContributionImpl contribution(
      PASSWORDS, sync_pb::DataTypeContext(), std::move(requests_data),
      /*on_commit_response_callback=*/base::NullCallback(),
      /*on_full_commit_failure_callback=*/base::NullCallback(),
      cryptographer.get(), PassphraseType::kImplicitPassphrase,
      /*only_commit_specifics=*/false);

  sync_pb::ClientToServerMessage msg;
  contribution.AddToCommitMessage(&msg);

  ASSERT_EQ(1, msg.commit().entries().size());
  SyncEntity entity = msg.commit().entries(0);

  // Exhaustively verify the populated SyncEntity.
  EXPECT_TRUE(entity.id_string().empty());
  EXPECT_EQ(7, entity.version());
  EXPECT_EQ("encrypted", entity.name());
  EXPECT_EQ(kTag.value(), entity.client_tag_hash());
  EXPECT_FALSE(entity.deleted());
  EXPECT_FALSE(entity.specifics().has_encrypted());
  EXPECT_TRUE(entity.specifics().has_password());
  EXPECT_EQ(kSignonRealm,
            entity.specifics().password().unencrypted_metadata().url());
  EXPECT_TRUE(
      entity.specifics().password().unencrypted_metadata().has_blacklisted());
  EXPECT_FALSE(
      entity.specifics().password().unencrypted_metadata().blacklisted());
  EXPECT_EQ(kDummyTimestamp, entity.specifics()
                                 .password()
                                 .unencrypted_metadata()
                                 .date_last_used_windows_epoch_micros());
  EXPECT_FALSE(entity.specifics().password().encrypted().blob().empty());
  EXPECT_TRUE(entity.parent_id_string().empty());
  EXPECT_FALSE(entity.unique_position().has_custom_compressed_v1());
}

// Same as above but uses CUSTOM_PASSPHRASE. In this case, field
// |unencrypted_metadata| should be cleared.
TEST(CommitContributionImplTest,
     PopulateCommitProtoPasswordWithCustomPassphrase) {
  const std::string kSignonRealm = "signon_realm";
  const int kDummyTimestamp = 123;
  const int64_t kBaseVersion = 7;

  auto data = std::make_unique<syncer::EntityData>();
  data->client_tag_hash = kTag;
  sync_pb::PasswordSpecificsData* password_data =
      data->specifics.mutable_password()->mutable_client_only_encrypted_data();
  password_data->set_signon_realm(kSignonRealm);

  data->specifics.mutable_password()->mutable_unencrypted_metadata()->set_url(
      kSignonRealm);
  data->specifics.mutable_password()
      ->mutable_unencrypted_metadata()
      ->set_blacklisted(false);
  data->specifics.mutable_password()
      ->mutable_unencrypted_metadata()
      ->set_date_last_used_windows_epoch_micros(kDummyTimestamp);

  auto request_data = std::make_unique<CommitRequestData>();
  request_data->sequence_number = 2;
  request_data->base_version = kBaseVersion;
  base::Base64Encode(base::SHA1HashString(data->specifics.SerializeAsString()),
                     &request_data->specifics_hash);
  request_data->entity = std::move(data);

  std::unique_ptr<FakeCryptographer> cryptographer =
      FakeCryptographer::FromSingleDefaultKey("dummy");

  CommitRequestDataList requests_data;
  requests_data.push_back(std::move(request_data));
  CommitContributionImpl contribution(
      PASSWORDS, sync_pb::DataTypeContext(), std::move(requests_data),
      /*on_commit_response_callback=*/base::NullCallback(),
      /*on_full_commit_failure_callback=*/base::NullCallback(),
      cryptographer.get(), PassphraseType::kCustomPassphrase,
      /*only_commit_specifics=*/false);

  sync_pb::ClientToServerMessage msg;
  contribution.AddToCommitMessage(&msg);

  ASSERT_EQ(1, msg.commit().entries().size());
  SyncEntity entity = msg.commit().entries(0);

  // Exhaustively verify the populated SyncEntity.
  EXPECT_TRUE(entity.id_string().empty());
  EXPECT_EQ(7, entity.version());
  EXPECT_EQ("encrypted", entity.name());
  EXPECT_EQ(kTag.value(), entity.client_tag_hash());
  EXPECT_FALSE(entity.deleted());
  EXPECT_FALSE(entity.specifics().has_encrypted());
  EXPECT_TRUE(entity.specifics().has_password());
  EXPECT_FALSE(entity.specifics().password().encrypted().blob().empty());
  EXPECT_FALSE(entity.specifics().password().has_unencrypted_metadata());
  EXPECT_TRUE(entity.parent_id_string().empty());
  EXPECT_FALSE(entity.unique_position().has_custom_compressed_v1());
}

TEST(CommitContributionImplTest, ShouldPropagateFailedItemsOnCommitResponse) {
  auto data = std::make_unique<syncer::EntityData>();
  data->client_tag_hash = ClientTagHash::FromHashed("hash");
  auto request_data = std::make_unique<CommitRequestData>();
  request_data->entity = std::move(data);
  CommitRequestDataList requests_data;
  requests_data.push_back(std::move(request_data));

  FakeCryptographer cryptographer;

  FailedCommitResponseDataList actual_error_response_list;

  auto on_commit_response_callback = base::BindOnce(
      [](FailedCommitResponseDataList* actual_error_response_list,
         const CommitResponseDataList& committed_response_list,
         const FailedCommitResponseDataList& error_response_list) {
        // We put expectations outside of the callback, so that they fail if
        // callback is not ran.
        *actual_error_response_list = error_response_list;
      },
      &actual_error_response_list);

  CommitContributionImpl contribution(
      PASSWORDS, sync_pb::DataTypeContext(), std::move(requests_data),
      std::move(on_commit_response_callback),
      /*on_full_commit_failure_callback=*/base::NullCallback(), &cryptographer,
      PassphraseType::kCustomPassphrase,
      /*only_commit_specifics=*/false);

  sync_pb::ClientToServerMessage msg;
  contribution.AddToCommitMessage(&msg);

  sync_pb::ClientToServerResponse response;
  sync_pb::CommitResponse* commit_response = response.mutable_commit();

  {
    sync_pb::CommitResponse_EntryResponse* entry =
        commit_response->add_entryresponse();
    entry->set_response_type(CommitResponse::TRANSIENT_ERROR);
    SharingMessageCommitError* sharing_message_error =
        entry->mutable_datatype_specific_error()
            ->mutable_sharing_message_error();
    sharing_message_error->set_error_code(
        SharingMessageCommitError::INVALID_ARGUMENT);
  }

  StatusController status;
  contribution.ProcessCommitResponse(response, &status);

  ASSERT_EQ(1u, actual_error_response_list.size());
  FailedCommitResponseData failed_item = actual_error_response_list[0];
  EXPECT_EQ(ClientTagHash::FromHashed("hash"), failed_item.client_tag_hash);
  EXPECT_EQ(CommitResponse::TRANSIENT_ERROR, failed_item.response_type);
  EXPECT_EQ(
      SharingMessageCommitError::INVALID_ARGUMENT,
      failed_item.datatype_specific_error.sharing_message_error().error_code());
}

TEST(CommitContributionImplTest, ShouldPropagateFullCommitFailure) {
  base::MockOnceCallback<void(SyncCommitError commit_error)>
      on_commit_failure_callback;
  EXPECT_CALL(on_commit_failure_callback, Run(SyncCommitError::kNetworkError));

  CommitContributionImpl contribution(
      BOOKMARKS, sync_pb::DataTypeContext(), CommitRequestDataList(),
      /*on_commit_response_callback=*/base::NullCallback(),
      on_commit_failure_callback.Get(), /*cryptographer=*/nullptr,
      PassphraseType::kKeystorePassphrase,
      /*only_commit_specifics=*/false);

  contribution.ProcessCommitFailure(SyncCommitError::kNetworkError);
}

TEST(CommitContributionImplTest, ShouldPopulatePasswordNotesBackup) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(syncer::kPasswordNotesWithBackup);

  const std::string kNoteValue = "Note Value";
  auto data = std::make_unique<syncer::EntityData>();
  data->client_tag_hash = kTag;
  sync_pb::PasswordSpecificsData* password_data =
      data->specifics.mutable_password()->mutable_client_only_encrypted_data();
  sync_pb::PasswordSpecificsData_Notes_Note* note =
      password_data->mutable_notes()->add_note();
  note->set_value(kNoteValue);

  auto request_data = std::make_unique<CommitRequestData>();
  base::Base64Encode(base::SHA1HashString(data->specifics.SerializeAsString()),
                     &request_data->specifics_hash);
  request_data->entity = std::move(data);

  CommitRequestDataList requests_data;
  requests_data.push_back(std::move(request_data));

  std::unique_ptr<FakeCryptographer> cryptographer =
      FakeCryptographer::FromSingleDefaultKey("dummy");
  CommitContributionImpl contribution(
      PASSWORDS, sync_pb::DataTypeContext(), std::move(requests_data),
      /*on_commit_response_callback=*/base::NullCallback(),
      /*on_full_commit_failure_callback=*/base::NullCallback(),
      cryptographer.get(), PassphraseType::kImplicitPassphrase,
      /*only_commit_specifics=*/false);

  sync_pb::ClientToServerMessage msg;
  contribution.AddToCommitMessage(&msg);

  ASSERT_EQ(1, msg.commit().entries().size());
  SyncEntity entity = msg.commit().entries(0);
  ASSERT_TRUE(entity.specifics().has_password());

  // Verify the contents of the encrypted notes backup blob.
  sync_pb::PasswordSpecificsData_Notes decrypted_notes;
  cryptographer->Decrypt(entity.specifics().password().encrypted_notes_backup(),
                         &decrypted_notes);
  ASSERT_EQ(1, decrypted_notes.note_size());
  EXPECT_EQ(kNoteValue, decrypted_notes.note(0).value());
}

TEST(CommitContributionImplTest,
     ShouldPopulatePasswordNotesBackupWhenNoLocalNotes) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(syncer::kPasswordNotesWithBackup);

  auto data = std::make_unique<syncer::EntityData>();
  data->client_tag_hash = kTag;
  sync_pb::PasswordSpecificsData* password_data =
      data->specifics.mutable_password()->mutable_client_only_encrypted_data();
  password_data->set_signon_realm("signon_realm");

  auto request_data = std::make_unique<CommitRequestData>();
  base::Base64Encode(base::SHA1HashString(data->specifics.SerializeAsString()),
                     &request_data->specifics_hash);
  request_data->entity = std::move(data);

  CommitRequestDataList requests_data;
  requests_data.push_back(std::move(request_data));

  std::unique_ptr<FakeCryptographer> cryptographer =
      FakeCryptographer::FromSingleDefaultKey("dummy");
  CommitContributionImpl contribution(
      PASSWORDS, sync_pb::DataTypeContext(), std::move(requests_data),
      /*on_commit_response_callback=*/base::NullCallback(),
      /*on_full_commit_failure_callback=*/base::NullCallback(),
      cryptographer.get(), PassphraseType::kImplicitPassphrase,
      /*only_commit_specifics=*/false);

  sync_pb::ClientToServerMessage msg;
  contribution.AddToCommitMessage(&msg);

  ASSERT_EQ(1, msg.commit().entries().size());
  SyncEntity entity = msg.commit().entries(0);
  ASSERT_TRUE(entity.specifics().has_password());
  EXPECT_FALSE(
      entity.specifics().password().encrypted_notes_backup().blob().empty());
}

}  // namespace

}  // namespace syncer