// 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/invalidation/impl/per_user_topic_subscription_manager.h"

#include <iostream>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "base/barrier_callback.h"
#include "base/byte_size.h"
#include "base/functional/bind.h"
#include "base/json/json_string_value_serializer.h"
#include "base/json/json_writer.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/values.h"
#include "components/invalidation/impl/profile_identity_provider.h"
#include "components/invalidation/public/invalidation_util.h"
#include "components/prefs/testing_pref_service.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

using testing::_;
using testing::Contains;
using testing::Eq;
using testing::IsEmpty;
using testing::NiceMock;
using testing::Not;
using testing::SizeIs;
using testing::UnorderedElementsAreArray;

namespace invalidation {

using RequestType = PerUserTopicSubscriptionManager::RequestType;

namespace {

constexpr size_t kInvalidationTopicsCount = 5;

const char kInvalidationRegistrationScope[] =
    "https://firebaseperusertopics-pa.googleapis.com";

const char kProjectId[] = "8181035976";

const char kTypeSubscribedForInvalidation[] =
    "invalidation.per_sender_registered_for_invalidation";

const char kActiveRegistrationTokens[] =
    "invalidation.per_sender_active_registration_tokens";

const char kFakeInstanceIdToken[] = "fake_instance_id_token";

class MockIdentityDiagnosticsObserver
    : public signin::IdentityManager::DiagnosticsObserver {
 public:
  MOCK_METHOD3(OnAccessTokenRequested,
               void(const CoreAccountId&,
                    const std::string&,
                    const signin::ScopeSet&));
  MOCK_METHOD2(OnAccessTokenRemovedFromCache,
               void(const CoreAccountId&, const signin::ScopeSet&));
};

std::string IndexToName(size_t index) {
  char name[2] = "a";
  name[0] += static_cast<char>(index);
  return name;
}

TopicMap GetSequenceOfTopicsStartingAt(size_t start, size_t count) {
  TopicMap topics;
  for (size_t i = start; i < start + count; ++i) {
    topics.emplace(IndexToName(i), TopicMetadata{false});
  }
  return topics;
}

TopicMap GetSequenceOfTopics(size_t count) {
  return GetSequenceOfTopicsStartingAt(0, count);
}

TopicSet TopicSetFromTopics(const TopicMap& topics) {
  TopicSet topic_set;
  for (const auto& topic : topics) {
    topic_set.insert(topic.first);
  }
  return topic_set;
}

network::mojom::URLResponseHeadPtr CreateHeadersForTest(int responce_code) {
  auto head = network::mojom::URLResponseHead::New();
  head->headers = new net::HttpResponseHeaders(base::StringPrintf(
      "HTTP/1.1 %d OK\nContent-type: text/html\n\n", responce_code));
  head->mime_type = "text/html";
  return head;
}

GURL FullSubscriptionUrl(const std::string& token) {
  return GURL(base::StringPrintf(
      "%s/v1/perusertopics/%s/rel/topics/?subscriber_token=%s",
      kInvalidationRegistrationScope, kProjectId, token.c_str()));
}

GURL FullUnSubscriptionUrlForTopic(const std::string& topic) {
  return GURL(base::StringPrintf(
      "%s/v1/perusertopics/%s/rel/topics/%s?subscriber_token=%s",
      kInvalidationRegistrationScope, kProjectId, topic.c_str(),
      kFakeInstanceIdToken));
}

network::URLLoaderCompletionStatus CreateStatusForTest(
    int status,
    const std::string& response_body) {
  network::URLLoaderCompletionStatus response_status(status);
  response_status.decoded_body_length = base::ByteSize(response_body.size());
  return response_status;
}

// SubscriptionRequestFinishedEvent instances are used to keep track of
// PerUserTopicSubscriptionManager::Observer::OnSubscriptionRequestFinished
// invocations.
struct SubscriptionRequestFinishedEvent {
  Topic topic;
  PerUserTopicSubscriptionManager::RequestType request_type;
  Status status;

  friend bool operator==(const SubscriptionRequestFinishedEvent& lhs,
                         const SubscriptionRequestFinishedEvent& rhs) = default;
  friend auto operator<=>(const SubscriptionRequestFinishedEvent& lhs,
                          const SubscriptionRequestFinishedEvent& rhs) =
      default;
};

std::ostream& operator<<(std::ostream& os,
                         const SubscriptionRequestFinishedEvent& event) {
  os << "SubscriptionRequestFinished{topic='" << event.topic
     << "', request_type=" << static_cast<int>(event.request_type)
     << ", status={code=" << static_cast<int>(event.status.code)
     << ", message='" << event.status.message << "'}}";
  return os;
}

// Returns a set of SubscriptionRequestFinishedEvent events of type
// `request_type` with the resulting Status `status`, one for each topic in
// `topics`.
std::multiset<SubscriptionRequestFinishedEvent>
SubscriptionRequestFinishedEvents(const TopicMap& topics,
                                  RequestType request_type,
                                  const Status& status) {
  std::multiset<SubscriptionRequestFinishedEvent> events;
  for (const auto& topic : topics) {
    events.insert(
        SubscriptionRequestFinishedEvent{topic.first, request_type, status});
  }
  return events;
}

}  // namespace

class RegistrationManagerStateObserver
    : public PerUserTopicSubscriptionManager::Observer {
 public:
  void OnSubscriptionChannelStateChanged(
      SubscriptionChannelState state) override {
    state_ = state;
    if (run_loop_) {
      run_loop_->Quit();
    }
  }

  void OnSubscriptionRequestFinished(
      Topic topic,
      PerUserTopicSubscriptionManager::RequestType request_type,
      Status status) override {
    subscription_request_finished_events_.insert(
        SubscriptionRequestFinishedEvent{topic, request_type, status});
    if (run_loop_) {
      run_loop_->Quit();
    }
  }

  void WaitForState(SubscriptionChannelState expected_state) {
    while (state_ != expected_state) {
      run_loop_ = std::make_unique<base::RunLoop>();
      run_loop_->Run();
      run_loop_.reset();
    }
  }

  // Waits for the recorded OnSubscriptionRequestFinished invocations to match
  // `expected`, then clears the recorded OnSubscriptionRequestFinished
  // invocations.
  // TODO - b/321195077: Prevent this from blocking in case `excepted.size()`
  // is too big.
  void WaitForSubscriptionRequestsFinished(
      const std::multiset<SubscriptionRequestFinishedEvent>& expected) {
    while (subscription_request_finished_events_.size() < expected.size()) {
      run_loop_ = std::make_unique<base::RunLoop>();
      run_loop_->Run();
      run_loop_.reset();
    }
    EXPECT_THAT(subscription_request_finished_events_,
                UnorderedElementsAreArray(expected));
    subscription_request_finished_events_.clear();
  }

  void ExpectNoSubscriptionRequestsFinished() {
    base::RunLoop().RunUntilIdle();
    EXPECT_THAT(subscription_request_finished_events_, IsEmpty());
  }

 private:
  SubscriptionChannelState state_ = SubscriptionChannelState::NOT_STARTED;
  std::unique_ptr<base::RunLoop> run_loop_;
  std::multiset<SubscriptionRequestFinishedEvent>
      subscription_request_finished_events_;
};

class PerUserTopicSubscriptionManagerTest : public testing::Test {
 protected:
  PerUserTopicSubscriptionManagerTest() = default;
  ~PerUserTopicSubscriptionManagerTest() override = default;

  void SetUp() override {
    PerUserTopicSubscriptionManager::RegisterProfilePrefs(
        pref_service_.registry());
    AccountInfo account = identity_test_env_.MakePrimaryAccountAvailable(
        "example@gmail.com", signin::ConsentLevel::kSignin);
    identity_test_env_.SetAutomaticIssueOfAccessTokens(true);
    identity_provider_ = std::make_unique<ProfileIdentityProvider>(
        identity_test_env_.identity_manager());
  }

  std::unique_ptr<PerUserTopicSubscriptionManager> BuildRegistrationManager() {
    auto reg_manager = std::make_unique<PerUserTopicSubscriptionManager>(
        identity_provider_.get(), &pref_service_, url_loader_factory(),
        kProjectId);
    reg_manager->Init();
    reg_manager->AddObserver(&state_observer_);
    return reg_manager;
  }

  network::TestURLLoaderFactory* url_loader_factory() {
    return &url_loader_factory_;
  }

  TestingPrefServiceSimple* pref_service() { return &pref_service_; }

  const base::DictValue& GetSubscribedTopics() const {
    const base::DictValue* subscribed_topics =
        pref_service_.GetDict(kTypeSubscribedForInvalidation)
            .FindDict(kProjectId);
    DCHECK(subscribed_topics);
    return *subscribed_topics;
  }

  void WaitForState(SubscriptionChannelState expected_state) {
    state_observer_.WaitForState(expected_state);
  }

  void WaitForSubscriptionRequestsFinished(
      const std::multiset<SubscriptionRequestFinishedEvent>& expected) {
    state_observer_.WaitForSubscriptionRequestsFinished(expected);
  }

  void ExpectNoSubscriptionRequestsFinished() {
    state_observer_.ExpectNoSubscriptionRequestsFinished();
  }

  void WaitForTopics(const PerUserTopicSubscriptionManager& manager,
                     const TopicMap& expected_topics) {
    while (manager.GetSubscribedTopicsForTest() !=
           TopicSetFromTopics(expected_topics)) {
      pref_service()->user_prefs_store()->WaitUntilValueChanges(
          kTypeSubscribedForInvalidation);
    }
  }

  void AddCorrectSubscriptionResponse(
      const std::string& private_topic = std::string(),
      const std::string& token = kFakeInstanceIdToken,
      int http_responce_code = net::HTTP_OK) {
    base::DictValue value;
    value.Set("privateTopicName",
              private_topic.empty() ? "test-pr" : private_topic.c_str());
    std::string serialized_response;
    JSONStringValueSerializer serializer(&serialized_response);
    serializer.Serialize(value);
    url_loader_factory()->AddResponse(
        FullSubscriptionUrl(token), CreateHeadersForTest(http_responce_code),
        serialized_response, CreateStatusForTest(net::OK, serialized_response));
  }

  void AddCorrectUnSubscriptionResponseForTopic(const std::string& topic) {
    url_loader_factory()->AddResponse(
        FullUnSubscriptionUrlForTopic(topic),
        CreateHeadersForTest(net::HTTP_OK), std::string() /* response_body */,
        CreateStatusForTest(net::OK, std::string() /* response_body */));
  }

  void FastForwardTimeBy(base::TimeDelta delta) {
    task_environment_.FastForwardBy(delta);
  }

  signin::IdentityTestEnvironment* identity_test_env() {
    return &identity_test_env_;
  }

 private:
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
  network::TestURLLoaderFactory url_loader_factory_;
  TestingPrefServiceSimple pref_service_;

  signin::IdentityTestEnvironment identity_test_env_;
  std::unique_ptr<ProfileIdentityProvider> identity_provider_;

  RegistrationManagerStateObserver state_observer_;
};

TEST_F(PerUserTopicSubscriptionManagerTest,
       EmptyPrivateTopicShouldNotUpdateSubscribedTopics) {
  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  auto per_user_topic_subscription_manager = BuildRegistrationManager();

  EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());

  // Empty response body should result in no successful registrations.
  std::string response_body;

  url_loader_factory()->AddResponse(
      FullSubscriptionUrl(kFakeInstanceIdToken),
      CreateHeadersForTest(net::HTTP_OK), response_body,
      CreateStatusForTest(net::OK, response_body));

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe,
      Status(StatusCode::FAILED, "Body missing")));

  // The response didn't contain non-empty topic name. So nothing was
  // registered.
  EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());
}

TEST_F(PerUserTopicSubscriptionManagerTest, ShouldUpdateSubscribedTopics) {
  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  auto per_user_topic_subscription_manager = BuildRegistrationManager();
  ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());

  AddCorrectSubscriptionResponse();

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  WaitForTopics(*per_user_topic_subscription_manager, topics);
  EXPECT_TRUE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  for (const auto& topic : topics) {
    const base::DictValue& subscribed_topics = GetSubscribedTopics();
    const std::string* private_topic_value =
        subscribed_topics.FindString(topic.first);
    ASSERT_NE(private_topic_value, nullptr);
  }
}

TEST_F(PerUserTopicSubscriptionManagerTest,
       ShouldNotRequestAccessTokenWhileHaveNoRequests) {
  NiceMock<MockIdentityDiagnosticsObserver> identity_observer;
  identity_test_env()->identity_manager()->AddDiagnosticsObserver(
      &identity_observer);

  EXPECT_CALL(identity_observer, OnAccessTokenRequested(_, _, _)).Times(0);
  BuildRegistrationManager()->UpdateSubscribedTopics({}, kFakeInstanceIdToken);

  identity_test_env()->identity_manager()->RemoveDiagnosticsObserver(
      &identity_observer);
}

TEST_F(PerUserTopicSubscriptionManagerTest, ShouldRepeatRequestsOnFailure) {
  // For this test, we want to manually control when access tokens are returned.
  identity_test_env()->SetAutomaticIssueOfAccessTokens(false);

  NiceMock<MockIdentityDiagnosticsObserver> identity_observer;
  identity_test_env()->identity_manager()->AddDiagnosticsObserver(
      &identity_observer);

  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  auto per_user_topic_subscription_manager = BuildRegistrationManager();
  ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());

  // The first subscription attempt will fail.
  AddCorrectSubscriptionResponse(
      /*private_topic=*/std::string(), kFakeInstanceIdToken,
      net::HTTP_INTERNAL_SERVER_ERROR);
  // Since this is a generic failure, not an auth error, the existing access
  // token should *not* get invalidated.
  EXPECT_CALL(identity_observer, OnAccessTokenRemovedFromCache(_, _)).Times(0);

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  // This should have resulted in a request for an access token. Return one.
  identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "access_token", base::Time::Max());

  // Wait for all of the subscription requests to fail. No retries
  // have been attempted yet.
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe,
      Status(StatusCode::FAILED, "HTTP Error: 500")));

  // Since the subscriptions failed, the requests should still be pending.
  EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());
  EXPECT_FALSE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  // The second attempt will succeed.
  AddCorrectSubscriptionResponse();

  // Repeating subscriptions shouldn't bypass backoff.
  // This should have resulted in a request for an access token. Return one.
  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "access_token", base::Time::Max());

  // Initial backoff is 2 seconds with 20% jitter, so the minimum possible delay
  // is 1600ms. Advance time to just before that; nothing should have changed
  // yet.
  FastForwardTimeBy(base::Milliseconds(1500));
  EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());
  EXPECT_FALSE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  // The maximum backoff is 2 seconds; advance to just past that.
  // Access token should be refreshed in order to avoid requests with expired
  // access token.
  EXPECT_CALL(identity_observer, OnAccessTokenRequested(_, _, _));
  FastForwardTimeBy(base::Milliseconds(600));
  identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "access_token", base::Time::Max());

  // Retries should be triggered now.
  // Wait for all of the subscription requests to finish.
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe, Status::Success()));

  // Now all subscriptions should have finished.
  EXPECT_FALSE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                   .empty());
  EXPECT_TRUE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  identity_test_env()->identity_manager()->RemoveDiagnosticsObserver(
      &identity_observer);
}

TEST_F(PerUserTopicSubscriptionManagerTest, ShouldNotRepeatOngoingRequests) {
  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  auto per_user_topic_subscription_manager = BuildRegistrationManager();

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  // The requests are not finished, so there should be one pending request per
  // invalidation topic.
  EXPECT_EQ(per_user_topic_subscription_manager
                ->GetPendingSubscriptionsCountForTest(),
            kInvalidationTopicsCount);

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);

  // No changes in wanted subscriptions or access token, so there should still
  // be only one pending request per invalidation topic.
  EXPECT_EQ(per_user_topic_subscription_manager
                ->GetPendingSubscriptionsCountForTest(),
            kInvalidationTopicsCount);
}

TEST_F(PerUserTopicSubscriptionManagerTest,
       ShouldRepeatAccessTokenRequestOnFailure) {
  // For this test, we want to manually control when access tokens are returned.
  identity_test_env()->SetAutomaticIssueOfAccessTokens(false);

  NiceMock<MockIdentityDiagnosticsObserver> identity_observer;
  identity_test_env()->identity_manager()->AddDiagnosticsObserver(
      &identity_observer);

  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  auto per_user_topic_subscription_manager = BuildRegistrationManager();
  ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());

  // Emulate failure on first access token request.
  EXPECT_CALL(identity_observer, OnAccessTokenRequested(_, _, _));
  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithError(
      GoogleServiceAuthError::FromConnectionError(net::ERR_FAILED));
  testing::Mock::VerifyAndClearExpectations(&identity_observer);

  // Initial backoff is 2 seconds with 20% jitter, so the minimum possible delay
  // is 1600ms. Advance time to just before that; nothing should have changed
  // yet.
  EXPECT_CALL(identity_observer, OnAccessTokenRequested(_, _, _)).Times(0);
  // UpdateSubscribedTopics() call shouldn't lead to backoff bypassing.
  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  FastForwardTimeBy(base::Milliseconds(1500));
  testing::Mock::VerifyAndClearExpectations(&identity_observer);

  // The maximum backoff is 2 seconds; advance to just past that. Now access
  // token should be requested.
  EXPECT_CALL(identity_observer, OnAccessTokenRequested(_, _, _));
  FastForwardTimeBy(base::Milliseconds(600));
  testing::Mock::VerifyAndClearExpectations(&identity_observer);

  // Add valid responses to access token and subscription requests and ensure
  // that subscription requests were successful.
  identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "valid_access_token", base::Time::Max());
  AddCorrectSubscriptionResponse();
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe, Status::Success()));
  EXPECT_FALSE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                   .empty());
  EXPECT_TRUE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  identity_test_env()->identity_manager()->RemoveDiagnosticsObserver(
      &identity_observer);
}

TEST_F(PerUserTopicSubscriptionManagerTest,
       ShouldInvalidateAccessTokenOnUnauthorized) {
  // For this test, we need to manually control when access tokens are returned.
  identity_test_env()->SetAutomaticIssueOfAccessTokens(false);

  NiceMock<MockIdentityDiagnosticsObserver> identity_observer;
  identity_test_env()->identity_manager()->AddDiagnosticsObserver(
      &identity_observer);

  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  auto per_user_topic_subscription_manager = BuildRegistrationManager();
  ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());

  // The first subscription attempt will fail with an "unauthorized" error.
  AddCorrectSubscriptionResponse(
      /*private_topic=*/std::string(), kFakeInstanceIdToken,
      net::HTTP_UNAUTHORIZED);
  // This error should result in invalidating the access token.
  EXPECT_CALL(identity_observer, OnAccessTokenRemovedFromCache(_, _));

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  // This should have resulted in a request for an access token. Return one
  // (which is considered invalid, e.g. already expired).
  identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "invalid_access_token", base::Time::Now());

  // Now the subscription requests should be scheduled.
  ASSERT_FALSE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  // Wait for the subscription requests to fail.
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe,
      Status(StatusCode::AUTH_FAILURE, "HTTP Error: 401")));

  // Since the subscriptions failed, the requests should still be pending.
  ASSERT_FALSE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  // A new access token should have been requested. Serving one will trigger
  // another subscription attempt; let this one succeed.
  AddCorrectSubscriptionResponse();
  EXPECT_CALL(identity_observer, OnAccessTokenRemovedFromCache(_, _)).Times(0);
  identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "valid_access_token", base::Time::Max());
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe, Status::Success()));

  EXPECT_FALSE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                   .empty());
  EXPECT_TRUE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  identity_test_env()->identity_manager()->RemoveDiagnosticsObserver(
      &identity_observer);
}

TEST_F(PerUserTopicSubscriptionManagerTest,
       ShouldInvalidateAccessTokenOnlyOnce) {
  // For this test, we need to manually control when access tokens are returned.
  identity_test_env()->SetAutomaticIssueOfAccessTokens(false);

  NiceMock<MockIdentityDiagnosticsObserver> identity_observer;
  identity_test_env()->identity_manager()->AddDiagnosticsObserver(
      &identity_observer);

  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  auto per_user_topic_subscription_manager = BuildRegistrationManager();
  ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());

  // The first subscription attempt will fail with an "unauthorized" error.
  AddCorrectSubscriptionResponse(
      /*private_topic=*/std::string(), kFakeInstanceIdToken,
      net::HTTP_UNAUTHORIZED);
  // This error should result in invalidating the access token.
  EXPECT_CALL(identity_observer, OnAccessTokenRemovedFromCache(_, _));

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  // This should have resulted in a request for an access token. Return one
  // (which is considered invalid, e.g. already expired).
  identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "invalid_access_token", base::Time::Now());

  // Now the subscription requests should be scheduled.
  ASSERT_FALSE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  // Wait for the subscription requests to fail.
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe,
      Status(StatusCode::AUTH_FAILURE, "HTTP Error: 401")));

  // Since the subscriptions failed, the requests should still be pending.
  ASSERT_FALSE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  // At this point, the old access token should have been invalidated and a new
  // one requested. The new one should *not* get invalidated.
  EXPECT_CALL(identity_observer, OnAccessTokenRemovedFromCache(_, _)).Times(0);
  // Serving a new access token will trigger another subscription attempt, but
  // the subscription requests will still fail with the same error.
  identity_test_env()->WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "invalid_access_token_2", base::Time::Max());
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe,
      Status(StatusCode::AUTH_FAILURE, "HTTP Error: 401")));

  // On the second auth failure, we should have given up - no new access token
  // request should have happened, and all the pending subscriptions should have
  // been dropped, even though still no topics are subscribed.
  EXPECT_FALSE(identity_test_env()->IsAccessTokenRequestPending());
  EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());
  EXPECT_TRUE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  identity_test_env()->identity_manager()->RemoveDiagnosticsObserver(
      &identity_observer);
}

TEST_F(PerUserTopicSubscriptionManagerTest,
       ShouldNotRepeatRequestsOnForbidden) {
  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  auto per_user_topic_subscription_manager = BuildRegistrationManager();
  ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());

  AddCorrectSubscriptionResponse(
      /*private_topic=*/std::string(), kFakeInstanceIdToken,
      net::HTTP_FORBIDDEN);

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe,
      Status(StatusCode::FAILED_NON_RETRIABLE, "HTTP Error: 403")));

  EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());
  EXPECT_TRUE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
}

TEST_F(PerUserTopicSubscriptionManagerTest,
       ShouldUnsubscribeTopicsAndDeleteFromPrefs) {
  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  AddCorrectSubscriptionResponse();

  auto per_user_topic_subscription_manager = BuildRegistrationManager();
  EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe, Status::Success()));
  EXPECT_EQ(TopicSetFromTopics(topics),
            per_user_topic_subscription_manager->GetSubscribedTopicsForTest());

  // Unsubscribe from some topics.
  auto unsubscribed_topics = GetSequenceOfTopics(3);
  auto still_subscribed_topics =
      GetSequenceOfTopicsStartingAt(3, kInvalidationTopicsCount - 3);
  for (const auto& topic : unsubscribed_topics) {
    AddCorrectUnSubscriptionResponseForTopic(topic.first);
  }

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      still_subscribed_topics, kFakeInstanceIdToken);
  // Expect the unsubscribe requests to start and succeed.
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      unsubscribed_topics, RequestType::kUnsubscribe, Status::Success()));

  // Topics were unsubscribed, check that they're not in the prefs.
  for (const auto& topic : unsubscribed_topics) {
    const base::DictValue& subscribed_topics = GetSubscribedTopics();
    const base::Value* private_topic_value =
        subscribed_topics.Find(topic.first);
    ASSERT_EQ(private_topic_value, nullptr);
  }

  // Check that still subscribed topics are still in the prefs.
  for (const auto& topic : still_subscribed_topics) {
    const base::DictValue& subscribed_topics = GetSubscribedTopics();
    const std::string* private_topic_value =
        subscribed_topics.FindString(topic.first);
    ASSERT_NE(private_topic_value, nullptr);
  }
}

TEST_F(PerUserTopicSubscriptionManagerTest,
       ShouldDropSavedTopicsOnTokenChange) {
  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  auto per_user_topic_subscription_manager = BuildRegistrationManager();

  EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());

  AddCorrectSubscriptionResponse("old-token-topic");

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  WaitForTopics(*per_user_topic_subscription_manager, topics);

  for (const auto& topic : topics) {
    const base::DictValue& subscribed_topics = GetSubscribedTopics();
    const std::string* private_topic_value =
        subscribed_topics.FindString(topic.first);
    ASSERT_NE(private_topic_value, nullptr);
    EXPECT_EQ("old-token-topic", *private_topic_value);
  }

  EXPECT_EQ(kFakeInstanceIdToken, *pref_service()
                                       ->GetDict(kActiveRegistrationTokens)
                                       .FindString(kProjectId));

  std::string token = "new-fake-token";
  AddCorrectSubscriptionResponse("new-token-topic", token);

  per_user_topic_subscription_manager->UpdateSubscribedTopics(topics, token);

  WaitForTopics(*per_user_topic_subscription_manager, topics);
  EXPECT_EQ(token, *pref_service()
                        ->GetDict(kActiveRegistrationTokens)
                        .FindString(kProjectId));

  for (const auto& topic : topics) {
    const base::DictValue& subscribed_topics = GetSubscribedTopics();
    const std::string* private_topic_value =
        subscribed_topics.FindString(topic.first);
    ASSERT_NE(private_topic_value, nullptr);
    EXPECT_EQ("new-token-topic", *private_topic_value);
  }
}

TEST_F(PerUserTopicSubscriptionManagerTest,
       ShouldDeleteTopicsFromPrefsWhenUnsubscribeRequestFails) {
  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  AddCorrectSubscriptionResponse();

  auto per_user_topic_subscription_manager = BuildRegistrationManager();
  EXPECT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe, Status::Success()));
  WaitForTopics(*per_user_topic_subscription_manager, topics);

  // Unsubscribe from some topics.
  auto unsubscribed_topics = GetSequenceOfTopics(3);
  auto still_subscribed_topics =
      GetSequenceOfTopicsStartingAt(3, kInvalidationTopicsCount - 3);
  // Without configuring the unsubscription response, the unsubscription
  // requests will not finish.
  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      still_subscribed_topics, kFakeInstanceIdToken);
  ExpectNoSubscriptionRequestsFinished();

  // Topics should be removed from prefs even though the unsubscribe requests
  // have not finished.
  for (const auto& topic : unsubscribed_topics) {
    const base::DictValue& subscribed_topics = GetSubscribedTopics();
    const base::Value* private_topic_value =
        subscribed_topics.Find(topic.first);
    ASSERT_EQ(private_topic_value, nullptr);
  }

  // Check that subscribed topics are still in the prefs.
  for (const auto& topic : still_subscribed_topics) {
    const base::DictValue& subscribed_topics = GetSubscribedTopics();
    const std::string* private_topic_value =
        subscribed_topics.FindString(topic.first);
    ASSERT_NE(private_topic_value, nullptr);
  }
}

TEST_F(PerUserTopicSubscriptionManagerTest,
       ShouldChangeStatusToEnabledWhenHasNoPendingSubscription) {
  BuildRegistrationManager()->UpdateSubscribedTopics(/*topics=*/{},
                                                     kFakeInstanceIdToken);
  WaitForState(SubscriptionChannelState::ENABLED);
}

TEST_F(PerUserTopicSubscriptionManagerTest,
       ShouldChangeStatusToDisabledWhenTopicsRegistrationFailed) {
  auto topics = GetSequenceOfTopics(kInvalidationTopicsCount);

  AddCorrectSubscriptionResponse();

  auto per_user_topic_subscription_manager = BuildRegistrationManager();
  ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  WaitForState(SubscriptionChannelState::ENABLED);
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      topics, RequestType::kSubscribe, Status::Success()));
  EXPECT_EQ(TopicSetFromTopics(topics),
            per_user_topic_subscription_manager->GetSubscribedTopicsForTest());

  // Unsubscribe from some topics.
  auto temporarily_unsubscribed_topics = GetSequenceOfTopics(3);
  auto still_subscribed_topics =
      GetSequenceOfTopicsStartingAt(3, kInvalidationTopicsCount - 3);
  for (const auto& topic : temporarily_unsubscribed_topics) {
    AddCorrectUnSubscriptionResponseForTopic(topic.first);
  }
  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      still_subscribed_topics, kFakeInstanceIdToken);
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      temporarily_unsubscribed_topics, RequestType::kUnsubscribe,
      Status::Success()));

  // Clear previously configured correct response, so next subscription requests
  // will fail. Then attempt to re-subscribe to the temporarily unsubscribed
  // topics.
  url_loader_factory()->ClearResponses();
  url_loader_factory()->AddResponse(
      FullSubscriptionUrl(kFakeInstanceIdToken).spec(),
      std::string() /* content */, net::HTTP_NOT_FOUND);

  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      temporarily_unsubscribed_topics, RequestType::kSubscribe,
      Status(StatusCode::FAILED_NON_RETRIABLE, "HTTP Error: 404")));
  WaitForState(SubscriptionChannelState::SUBSCRIPTION_FAILURE);

  // Configure correct response and attempt again to re-subscribe to the
  // temporarily unsubscribed topics.
  AddCorrectSubscriptionResponse();
  per_user_topic_subscription_manager->UpdateSubscribedTopics(
      topics, kFakeInstanceIdToken);
  WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
      temporarily_unsubscribed_topics, RequestType::kSubscribe,
      Status::Success()));
  WaitForState(SubscriptionChannelState::ENABLED);
}

TEST_F(PerUserTopicSubscriptionManagerTest, ShouldRecordTokenStateHistogram) {
  const char kTokenStateHistogram[] =
      "FCMInvalidations.TokenStateOnRegistrationRequest2";
  enum class TokenStateOnSubscriptionRequest {
    kTokenWasEmpty = 0,
    kTokenUnchanged = 1,
    kTokenChanged = 2,
    kTokenCleared = 3,
  };

  const TopicMap topics = GetSequenceOfTopics(kInvalidationTopicsCount);
  auto per_user_topic_subscription_manager = BuildRegistrationManager();

  // Subscribe to some topics (and provide an InstanceID token).
  {
    base::HistogramTester histograms;

    AddCorrectSubscriptionResponse(/*private_topic=*/"", "original_token");
    per_user_topic_subscription_manager->UpdateSubscribedTopics(
        topics, "original_token");
    WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
        topics, RequestType::kSubscribe, Status::Success()));

    histograms.ExpectUniqueSample(
        kTokenStateHistogram, TokenStateOnSubscriptionRequest::kTokenWasEmpty,
        1);
  }

  ASSERT_EQ(TopicSetFromTopics(topics),
            per_user_topic_subscription_manager->GetSubscribedTopicsForTest());
  ASSERT_TRUE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  // Call UpdateSubscribedTopics again with the same token.
  {
    base::HistogramTester histograms;

    per_user_topic_subscription_manager->UpdateSubscribedTopics(
        topics, "original_token");

    // Nothing happens, so no need to wait for anything.

    histograms.ExpectUniqueSample(
        kTokenStateHistogram, TokenStateOnSubscriptionRequest::kTokenUnchanged,
        1);
  }

  // Topic subscriptions are unchanged.
  ASSERT_EQ(TopicSetFromTopics(topics),
            per_user_topic_subscription_manager->GetSubscribedTopicsForTest());
  ASSERT_TRUE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  // Call UpdateSubscribedTopics again, but now with a different token.
  {
    base::HistogramTester histograms;

    AddCorrectSubscriptionResponse(/*private_topic=*/"", "different_token");
    per_user_topic_subscription_manager->UpdateSubscribedTopics(
        topics, "different_token");
    WaitForSubscriptionRequestsFinished(SubscriptionRequestFinishedEvents(
        topics, RequestType::kSubscribe, Status::Success()));

    histograms.ExpectUniqueSample(
        kTokenStateHistogram, TokenStateOnSubscriptionRequest::kTokenChanged,
        1);
  }

  // Topic subscriptions are still the same (all topics were re-subscribed).
  ASSERT_EQ(TopicSetFromTopics(topics),
            per_user_topic_subscription_manager->GetSubscribedTopicsForTest());
  ASSERT_TRUE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());

  // Call ClearInstanceIDToken.
  {
    base::HistogramTester histograms;

    per_user_topic_subscription_manager->ClearInstanceIDToken();
    histograms.ExpectUniqueSample(
        kTokenStateHistogram, TokenStateOnSubscriptionRequest::kTokenCleared,
        1);
  }

  // Topic subscriptions are gone now.
  ASSERT_TRUE(per_user_topic_subscription_manager->GetSubscribedTopicsForTest()
                  .empty());
  ASSERT_TRUE(
      per_user_topic_subscription_manager->HaveAllRequestsFinishedForTest());
}

}  // namespace invalidation
