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

#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/browser/sync/test/integration/autofill_helper.h"
#include "chrome/browser/sync/test/integration/offer_helper.h"
#include "chrome/browser/sync/test/integration/sync_service_impl_harness.h"
#include "chrome/browser/sync/test/integration/sync_test.h"
#include "chrome/browser/sync/test/integration/wallet_helper.h"
#include "components/autofill/core/browser/data_manager/payments/payments_data_manager.h"
#include "components/autofill/core/browser/data_manager/payments/payments_data_manager_test_utils.h"
#include "components/autofill/core/browser/data_model/payments/autofill_offer_data.h"
#include "components/autofill/core/browser/test_utils/autofill_test_utils.h"
#include "components/sync/base/data_type.h"
#include "components/sync/base/features.h"
#include "components/sync/engine/loopback_server/persistent_tombstone_entity.h"
#include "components/sync/protocol/data_type_state.pb.h"
#include "components/sync/service/sync_service.h"
#include "components/sync/test/fake_server.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"

namespace {

using autofill::AutofillOfferData;
using autofill::PaymentsDataChangedWaiter;
using autofill::PaymentsDataManager;
using autofill::test::GetCardLinkedOfferData1;
using autofill::test::GetCardLinkedOfferData2;
using offer_helper::CreateDefaultSyncCardLinkedOffer;
using offer_helper::CreateSyncCardLinkedOffer;
using offer_helper::SetOfferData;
using wallet_helper::GetPaymentsDataManager;
using wallet_helper::GetWalletDataTypeState;

ACTION_P(QuitMessageLoop, loop) {
  loop->Quit();
}

}  // namespace

class SingleClientOfferSyncTest
    : public SyncTest,
      public testing::WithParamInterface<SyncTest::SetupSyncMode> {
 public:
  SingleClientOfferSyncTest() : SyncTest(SINGLE_CLIENT) {
    if (GetSetupSyncMode() == SetupSyncMode::kSyncTransportOnly) {
      scoped_feature_list_.InitAndEnableFeature(
          syncer::kReplaceSyncPromosWithSignInPromos);
    }
  }

  ~SingleClientOfferSyncTest() override = default;

  SingleClientOfferSyncTest(const SingleClientOfferSyncTest&) = delete;
  SingleClientOfferSyncTest& operator=(const SingleClientOfferSyncTest&) =
      delete;

  SyncTest::SetupSyncMode GetSetupSyncMode() const override {
    return GetParam();
  }

 protected:
  wallet_helper::StoreType GetStoreType() const {
    return GetSetupSyncMode() == SyncTest::SetupSyncMode::kSyncTransportOnly
               ? wallet_helper::StoreType::kAccountStore
               : wallet_helper::StoreType::kProfileStore;
  }

  void WaitForNumberOfOffers(size_t expected_count,
                             autofill::PaymentsDataManager* paydm) {
    while (paydm->GetAutofillOffers().size() != expected_count ||
           paydm->HasPendingPaymentQueries()) {
      PaymentsDataChangedWaiter(paydm).Wait();
    }
  }

  bool TriggerGetUpdatesAndWait() {
    const base::Time now = base::Time::Now();
    // Trigger a sync and wait for the new data to arrive.
    TriggerSyncForDataTypes(0, {syncer::AUTOFILL_WALLET_OFFER});
    return FullUpdateTypeProgressMarkerChecker(now, GetSyncService(0),
                                               syncer::AUTOFILL_WALLET_OFFER)
        .Wait();
  }

  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(
    /* no prefix */,
    SingleClientOfferSyncTest,
    GetSyncTestModes(),
    testing::PrintToStringParamName());

// Ensures that the offer sync type is enabled by default.
IN_PROC_BROWSER_TEST_P(SingleClientOfferSyncTest, EnabledByDefault) {
  ASSERT_TRUE(SetupSync());
  ASSERT_TRUE(GetClient(0)->service()->GetActiveDataTypes().Has(
      syncer::AUTOFILL_WALLET_OFFER));
}

// Ensures that offer data should get cleared from the database when sync is
// (temporarily) stopped, e.g. due to a persistent auth error.
//
// Excluded on Android because SyncServiceImplHarness doesn't have the ability
// to mimic sync-paused on Android due to https://crbug.com/40871747.
#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_P(SingleClientOfferSyncTest, ClearOnSyncPaused) {
  SetOfferData(GetFakeServer(), {CreateDefaultSyncCardLinkedOffer()});
  ASSERT_TRUE(SetupSync());

  autofill::PaymentsDataManager* paydm = GetPaymentsDataManager(0);
  ASSERT_NE(nullptr, paydm);
  // Make sure the offer data is in the DB.
  ASSERT_EQ(1uL, paydm->GetAutofillOffers().size());

  // Pause sync, the offer data should be gone.
  if (GetSetupSyncMode() == SetupSyncMode::kSyncTransportOnly) {
    GetClient(0)->EnterSignInPendingStateForPrimaryAccount();
  } else {
    GetClient(0)->EnterSyncPausedStateForPrimaryAccount();
  }
  WaitForNumberOfOffers(0, paydm);
  EXPECT_EQ(0uL, paydm->GetAutofillOffers().size());

  // Resume (unpause) sync, the data should come back.
  if (GetSetupSyncMode() == SetupSyncMode::kSyncTransportOnly) {
    GetClient(0)->ExitSignInPendingStateForPrimaryAccount();
  } else {
    GetClient(0)->ExitSyncPausedStateForPrimaryAccount();
  }
  // Wait until Sync restores the card and it arrives at paydm.
  WaitForNumberOfOffers(1, paydm);
  EXPECT_EQ(1uL, paydm->GetAutofillOffers().size());
}
#endif  // !BUILDFLAG(IS_ANDROID)

// ChromeOS does not sign out, so the test below does not apply.
#if !BUILDFLAG(IS_CHROMEOS)
// Offer data should get cleared from the database when the user signs out.
IN_PROC_BROWSER_TEST_P(SingleClientOfferSyncTest, ClearOnSignOut) {
  SetOfferData(GetFakeServer(), {CreateDefaultSyncCardLinkedOffer()});
  ASSERT_TRUE(SetupSync());
  autofill::PaymentsDataManager* paydm = GetPaymentsDataManager(0);
  ASSERT_NE(nullptr, paydm);
  // Make sure the data & metadata is in the DB.
  ASSERT_EQ(1uL, paydm->GetAutofillOffers().size());

  // Signout, the data & metadata should be gone.
  GetClient(0)->SignOutPrimaryAccount();
  WaitForNumberOfOffers(0, paydm);
  EXPECT_EQ(0uL, paydm->GetAutofillOffers().size());
}
#endif  // !BUILDFLAG(IS_CHROMEOS)

// Offer is not using incremental updates. Make sure existing data gets
// replaced when synced down.
IN_PROC_BROWSER_TEST_P(SingleClientOfferSyncTest,
                       NewSyncDataShouldReplaceExistingData) {
  AutofillOfferData offer1 = GetCardLinkedOfferData1(/*offer_id=*/999);
  SetOfferData(GetFakeServer(), {CreateSyncCardLinkedOffer(offer1)});
  ASSERT_TRUE(SetupSync());

  // Make sure the data is in the DB.
  PaymentsDataManager* paydm = GetPaymentsDataManager(0);
  ASSERT_NE(nullptr, paydm);
  std::vector<const AutofillOfferData*> offers = paydm->GetAutofillOffers();
  ASSERT_EQ(1uL, offers.size());
  EXPECT_EQ(999, offers[0]->GetOfferId());

  // Put some completely new data in the sync server.
  AutofillOfferData offer2 = GetCardLinkedOfferData2(/*offer_id=*/888);
  SetOfferData(GetFakeServer(), {CreateSyncCardLinkedOffer(offer2)});
  PaymentsDataChangedWaiter(paydm).Wait();

  // Make sure only the new data is present.
  offers = paydm->GetAutofillOffers();
  ASSERT_EQ(1uL, offers.size());
  EXPECT_EQ(888, offers[0]->GetOfferId());
}

// Offer is not using incremental updates. The server either sends a non-empty
// update with deletion gc directives and with the (possibly empty) full data
// set, or (more often) an empty update.
IN_PROC_BROWSER_TEST_P(SingleClientOfferSyncTest, EmptyUpdatesAreIgnored) {
  AutofillOfferData offer1 = GetCardLinkedOfferData1(/*offer_id=*/999);
  SetOfferData(GetFakeServer(), {CreateSyncCardLinkedOffer(offer1)});
  ASSERT_TRUE(SetupSync());

  // Make sure the card is in the DB.
  PaymentsDataManager* paydm = GetPaymentsDataManager(0);
  ASSERT_NE(nullptr, paydm);
  std::vector<const AutofillOfferData*> offers = paydm->GetAutofillOffers();
  ASSERT_EQ(1uL, offers.size());
  EXPECT_EQ(999, offers[0]->GetOfferId());

  // Trigger a sync and wait for the new data to arrive.
  sync_pb::DataTypeState state_before =
      GetWalletDataTypeState(syncer::AUTOFILL_WALLET_OFFER, 0, GetStoreType());

  // Inject a tombstone (deleted entity) on the server to advance the server
  // version for AUTOFILL_WALLET_OFFER. LoopbackServer will return an empty
  // update (0 entities) but advance the progress marker token version.
  GetFakeServer()->InjectEntity(
      syncer::PersistentTombstoneEntity::CreateNewForTest(
          syncer::AUTOFILL_WALLET_OFFER, "tombstone-id"));

  ASSERT_TRUE(TriggerGetUpdatesAndWait());

  // Check that the new progress marker is stored for empty updates. This is a
  // regression check for crbug.com/41436932.
  sync_pb::DataTypeState state_after =
      GetWalletDataTypeState(syncer::AUTOFILL_WALLET_OFFER, 0, GetStoreType());
  EXPECT_NE(state_before.progress_marker().token(),
            state_after.progress_marker().token());

  // Refresh the paydm to make sure we are checking its state after any
  // potential changes from sync in the DB propagate into paydm. As we don't
  // expect anything to change, we have no better specific condition to wait
  // for.
  paydm->Refresh();
  while (paydm->HasPendingPaymentQueries()) {
    PaymentsDataChangedWaiter(paydm).Wait();
  }

  // Make sure the same data is present on the client.
  offers = paydm->GetAutofillOffers();
  ASSERT_EQ(1uL, offers.size());
  EXPECT_EQ(999, offers[0]->GetOfferId());
}

// If the server sends the same offers with changed data, they should change on
// the client.
IN_PROC_BROWSER_TEST_P(SingleClientOfferSyncTest, ChangedEntityGetsUpdated) {
  AutofillOfferData offer = GetCardLinkedOfferData1(/*offer_id=*/999);
  offer.SetEligibleInstrumentIdForTesting({111111});
  SetOfferData(GetFakeServer(), {CreateSyncCardLinkedOffer(offer)});
  ASSERT_TRUE(SetupSync());

  // Make sure the card is in the DB.
  PaymentsDataManager* paydm = GetPaymentsDataManager(0);
  ASSERT_NE(nullptr, paydm);
  std::vector<const AutofillOfferData*> offers = paydm->GetAutofillOffers();
  ASSERT_EQ(1uL, offers.size());
  EXPECT_EQ(999, offers[0]->GetOfferId());
  EXPECT_EQ(1U, offers[0]->GetEligibleInstrumentIds().size());

  // Update the data.
  offer.SetEligibleInstrumentIdForTesting({111111, 222222});
  SetOfferData(GetFakeServer(), {CreateSyncCardLinkedOffer(offer)});
  PaymentsDataChangedWaiter(paydm).Wait();

  // Make sure the data is present on the client.
  paydm = GetPaymentsDataManager(0);
  ASSERT_NE(nullptr, paydm);
  offers = paydm->GetAutofillOffers();
  ASSERT_EQ(1uL, offers.size());
  EXPECT_EQ(999, offers[0]->GetOfferId());
  EXPECT_EQ(2U, offers[0]->GetEligibleInstrumentIds().size());
}

// Offer data should get cleared from the database when the Autofill sync type
// flag is disabled.
IN_PROC_BROWSER_TEST_P(SingleClientOfferSyncTest, ClearOnDisableWalletSync) {
  SetOfferData(GetFakeServer(), {CreateDefaultSyncCardLinkedOffer()});
  ASSERT_TRUE(SetupSync());

  PaymentsDataManager* paydm = GetPaymentsDataManager(0);
  ASSERT_NE(nullptr, paydm);
  // Make sure the data is in the DB.
  ASSERT_EQ(1uL, paydm->GetAutofillOffers().size());

  // Turn off payments sync, the data should be gone.
  ASSERT_TRUE(GetClient(0)->DisableSelectableType(
      syncer::UserSelectableType::kPayments));
  WaitForNumberOfOffers(0, paydm);
  EXPECT_EQ(0uL, paydm->GetAutofillOffers().size());
}
