// 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 "chrome/browser/updates/announcement_notification/announcement_notification_service.h"

#include <memory>

#include "base/files/file_path.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/to_string.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/profiles/profile_attributes_init_params.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/account_id/account_id.h"
#include "components/prefs/testing_pref_service.h"
#include "components/sync_preferences/pref_service_syncable.h"
#include "content/public/test/browser_task_environment.h"
#include "google_apis/gaia/gaia_id.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using testing::_;
using testing::NiceMock;
using testing::Return;

namespace {

const char kProfileId[] = "dummy@gmail.com";
const char kRemoteUrl[] = "www.example.com";

class MockDelegate : public AnnouncementNotificationService::Delegate {
 public:
  MockDelegate() = default;

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

  ~MockDelegate() override = default;
  MOCK_METHOD0(ShowNotification, void());
  MOCK_METHOD0(IsFirstRun, bool());
};

class AnnouncementNotificationServiceTest : public testing::Test {
 public:
  AnnouncementNotificationServiceTest() = default;

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

  ~AnnouncementNotificationServiceTest() override = default;

 protected:
  AnnouncementNotificationService* service() {
    DCHECK(service_) << "Call Init() first.";
    return service_.get();
  }

  MockDelegate* delegate() { return delegate_; }

  base::SimpleTestClock* clock() { return &clock_; }

  int CurrentVersionPref() const {
    return pref_service_->GetInteger(kCurrentVersionPrefName);
  }

  base::Time FirstRunTimePref() const {
    return pref_service_->GetTime(kAnnouncementFirstRunTimePrefName);
  }

  base::Time SetFirstRunTimePref(const char* time_str) {
    base::Time time;
    EXPECT_TRUE(base::Time::FromString(time_str, &time));
    pref_service_->SetTime(kAnnouncementFirstRunTimePrefName, time);
    return time;
  }

  // Sets the current time used in test. Assume UTC time when time zone is not
  // specified.
  base::Time SetNow(const char* now_str) {
    base::Time now;
    EXPECT_TRUE(base::Time::FromUTCString(now_str, &now));
    clock()->SetNow(now);
    EXPECT_FALSE(now.is_null());
    return now;
  }

  void Init(const std::map<std::string, std::string>& parameters,
            bool enable_feature,
            bool sign_in,
            int current_version,
            bool new_profile,
            bool guest_profile = false) {
    std::vector<base::test::FeatureRefAndParams> enabled_features;
    std::vector<base::test::FeatureRef> disabled_features;
    if (enable_feature)
      enabled_features.emplace_back(kAnnouncementNotification, parameters);
    else
      disabled_features.push_back(kAnnouncementNotification);

    scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features,
                                                       disabled_features);

    // Setup sign in status.
    test_profile_manager_ = std::make_unique<TestingProfileManager>(
        TestingBrowserProcess::GetGlobal());
    ASSERT_TRUE(test_profile_manager_->SetUp());

    // Build the testing profile.
    TestingProfile::Builder builder;
    builder.SetPath(
        test_profile_manager_->profiles_dir().AppendASCII(kProfileId));
    builder.SetPrefService(
        std::unique_ptr<sync_preferences::PrefServiceSyncable>());
    builder.SetProfileName(kProfileId);
    builder.SetIsNewProfile(new_profile);
    if (guest_profile)
      builder.SetGuestSession();
    test_profile_ = builder.Build();

    // Mock the sign in profile data.
    DCHECK_EQ(test_profile_->GetPath(),
              test_profile_manager_->profiles_dir().AppendASCII(kProfileId));
    ProfileAttributesInitParams params;
    params.profile_path =
        test_profile_manager_->profiles_dir().AppendASCII(kProfileId);
    params.profile_name = u"dummy_name";
    params.gaia_id = sign_in ? GaiaId("dummy_gaia_id") : GaiaId();
    params.is_consented_primary_account = sign_in;
    test_profile_manager_->profile_attributes_storage()->AddProfile(
        std::move(params));

    // Register pref.
    pref_service_ = std::make_unique<TestingPrefServiceSimple>();
    AnnouncementNotificationService::RegisterProfilePrefs(
        pref_service_->registry());
    pref_service_->SetInteger(kCurrentVersionPrefName, current_version);

    // Setup test target objects.
    auto delegate = std::make_unique<NiceMock<MockDelegate>>();
    delegate_ = delegate.get();
    service_ = AnnouncementNotificationService::Create(
        test_profile_.get(), pref_service_.get(), std::move(delegate), &clock_);
  }

 private:
  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<TestingProfileManager> test_profile_manager_;
  base::SimpleTestClock clock_;
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<AnnouncementNotificationService> service_;
  std::unique_ptr<TestingPrefServiceSimple> pref_service_;
  std::unique_ptr<TestingProfile> test_profile_;
  raw_ptr<MockDelegate> delegate_ = nullptr;
};

TEST_F(AnnouncementNotificationServiceTest, RequireSignOut) {
  std::map<std::string, std::string> parameters = {
      {kSkipFirstRun, "false"}, {kVersion, "2"}, {kRequireSignout, "true"}};
  // Profile now is signed in.
  Init(parameters, true, true /*sign_in*/, 1, false);

  ON_CALL(*delegate(), IsFirstRun()).WillByDefault(Return(false));
  EXPECT_CALL(*delegate(), ShowNotification()).Times(0);
  service()->MaybeShowNotification();
  EXPECT_EQ(CurrentVersionPref(), 2);
}

TEST_F(AnnouncementNotificationServiceTest, SkipNewProfile) {
  std::map<std::string, std::string> parameters = {
      {kSkipFirstRun, "false"}, {kVersion, "2"}, {kSkipNewProfile, "true"}};
  Init(parameters, true, false, 1, true /*new_profile*/);

  ON_CALL(*delegate(), IsFirstRun()).WillByDefault(Return(false));
  EXPECT_CALL(*delegate(), ShowNotification()).Times(0);
  service()->MaybeShowNotification();
  EXPECT_EQ(CurrentVersionPref(), 2);
}

TEST_F(AnnouncementNotificationServiceTest, SkipGuestProfile) {
  std::map<std::string, std::string> parameters = {
      {kSkipFirstRun, "false"}, {kVersion, "2"}, {kSkipNewProfile, "false"}};
  Init(parameters, true, false, 1, false, /*guest_profile=*/true);
  ON_CALL(*delegate(), IsFirstRun()).WillByDefault(Return(false));
  EXPECT_CALL(*delegate(), ShowNotification()).Times(0);
  service()->MaybeShowNotification();
  EXPECT_EQ(CurrentVersionPref(), 2);
}

TEST_F(AnnouncementNotificationServiceTest, RemoteUrl) {
  std::map<std::string, std::string> parameters = {
      {kSkipFirstRun, "false"},
      {kVersion, "4"},
      {kSkipNewProfile, "true"},
      {kAnnouncementUrl, kRemoteUrl}};
  Init(parameters, true, false, 1, false);
  ON_CALL(*delegate(), IsFirstRun()).WillByDefault(Return(false));
  EXPECT_CALL(*delegate(), ShowNotification());
  service()->MaybeShowNotification();
  EXPECT_EQ(CurrentVersionPref(), 4);
}

// First run timestamp should be persisted even if the Finch parameter is not
// received or the feature is disabled.
TEST_F(AnnouncementNotificationServiceTest, SaveFirstRunTimeOnFirstRun) {
  base::Time now = SetNow("30 May 2018 12:00:00");
  std::map<std::string, std::string> parameters;
  Init(parameters, false /*enable_feature*/, false, -1, true);
  ON_CALL(*delegate(), IsFirstRun()).WillByDefault(Return(true));
  EXPECT_CALL(*delegate(), ShowNotification()).Times(0);
  service()->MaybeShowNotification();
  EXPECT_EQ(CurrentVersionPref(), -1);
  EXPECT_EQ(FirstRunTimePref(), now);
}

// First run timestamp should not be persisted when not on first run.
TEST_F(AnnouncementNotificationServiceTest, SaveFirstRunTimeNotFirstRun) {
  SetNow("30 May 2018 12:00:00");
  std::map<std::string, std::string> parameters;
  Init(parameters, false /*enable_feature*/, false, -1, true);
  ON_CALL(*delegate(), IsFirstRun()).WillByDefault(Return(false));
  service()->MaybeShowNotification();
  EXPECT_EQ(CurrentVersionPref(), -1);
  EXPECT_EQ(FirstRunTimePref(), base::Time());
}

// Not to show notification if first run timestamp happens after Finch parameter
// timestamp.
TEST_F(AnnouncementNotificationServiceTest, SkipFirstRunAfterTimeNotShow) {
  SetNow("10 Feb 2020 13:00:00 GMT");
  std::map<std::string, std::string> parameters = {
      {kSkipFirstRun, "false"},
      {kVersion, "2"},
      {kSkipNewProfile, "false"},
      {kSkipFirstRunAfterTime, "10 Feb 2020 12:15:00 GMT"}};
  Init(parameters, true, false, 1, false);
  auto first_run_time = SetFirstRunTimePref("10 Feb 2020 12:30:00 GMT");
  ON_CALL(*delegate(), IsFirstRun()).WillByDefault(Return(false));
  EXPECT_CALL(*delegate(), ShowNotification()).Times(0);
  service()->MaybeShowNotification();
  EXPECT_EQ(CurrentVersionPref(), 2);
  EXPECT_EQ(FirstRunTimePref(), first_run_time);
}

// Show notification if first run timestamp happens before Finch parameter
// timestamp.
TEST_F(AnnouncementNotificationServiceTest, SkipFirstRunAfterTimeShow) {
  SetNow("10 Feb 2020 13:00:00 GMT");
  std::map<std::string, std::string> parameters = {
      {kSkipFirstRun, "false"},
      {kVersion, "2"},
      {kSkipNewProfile, "false"},
      {kSkipFirstRunAfterTime, "10 Feb 2020 12:15:00 GMT"}};
  Init(parameters, true, false, 1, false);
  SetFirstRunTimePref("10 Feb 2020 12:10:00 GMT");
  ON_CALL(*delegate(), IsFirstRun()).WillByDefault(Return(false));
  EXPECT_CALL(*delegate(), ShowNotification());
  service()->MaybeShowNotification();
  EXPECT_EQ(CurrentVersionPref(), 2);
}

// Show notification if there is no first run timestamp but Finch has
// "skip_first_run_after_time" parameter.
TEST_F(AnnouncementNotificationServiceTest, SkipFirstRunAfterNoFirstRunPref) {
  SetNow("10 Feb 2020 13:00:00 GMT");
  std::map<std::string, std::string> parameters = {
      {kSkipFirstRun, "false"},
      {kVersion, "2"},
      {kSkipNewProfile, "false"},
      {kSkipFirstRunAfterTime, "10 Feb 2020 12:15:00 GMT"}};
  Init(parameters, true, false, 1, false);
  EXPECT_EQ(FirstRunTimePref(), base::Time());
  ON_CALL(*delegate(), IsFirstRun()).WillByDefault(Return(false));
  EXPECT_CALL(*delegate(), ShowNotification());
  service()->MaybeShowNotification();
  EXPECT_EQ(CurrentVersionPref(), 2);
  EXPECT_EQ(FirstRunTimePref(), base::Time());
}

// "skip_first_run_after_time" parameter should assume UTC when time zone is not
// specified.
TEST_F(AnnouncementNotificationServiceTest, SkipFirstRunAfterAssumeUTCTime) {
  SetNow("10 Feb 2020 13:00:00 GMT");
  std::map<std::string, std::string> parameters = {
      {kSkipFirstRun, "false"},
      {kVersion, "2"},
      {kSkipNewProfile, "false"},
      // No time zone specified. The time string should assume UTC.
      {kSkipFirstRunAfterTime, "10 Feb 2020 12:59:59"}};
  Init(parameters, true, false, 1, false);
  EXPECT_EQ(FirstRunTimePref(), base::Time());
  ON_CALL(*delegate(), IsFirstRun()).WillByDefault(Return(false));
  EXPECT_CALL(*delegate(), ShowNotification());
  service()->MaybeShowNotification();
}

struct VersionTestParam {
  bool enable_feature;
  bool skip_first_run;
  bool is_first_run;
  int version;
  int current_version;
  bool show_notification_called;
  int expected_version_pref;
};

class AnnouncementNotificationServiceVersionTest
    : public AnnouncementNotificationServiceTest,
      public ::testing::WithParamInterface<VersionTestParam> {
 public:
  AnnouncementNotificationServiceVersionTest() = default;

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

  ~AnnouncementNotificationServiceVersionTest() override = default;
};

const VersionTestParam kVersionTestParams[] = {
    // First run. No current version in pref, has Finch parameter.
    {true, false /*skip_first_run*/, true /*is_first_run*/, 1, -1, true, 1},
    // Skip first run.
    {true, true /*skip_first_run*/, true /*is_first_run*/, 2, -1, false, 2},
    {true, true /*skip_first_run*/, false /*is_first_run*/, 2, -1, true, 2},
    // DisableFeature
    {false /*enable_feature*/, false, false, 1, -1, false, -1},
    // Same version between Finch parameter and preference.
    {true, false, false, 3 /*version*/, 3 /*current_version*/, false, 3},
    // New version from Finch parameter.
    {true, false, false, 4 /*version*/, 3 /*current_version*/, true, 4},
    // OldVersion
    {true, false, false, 2 /*version*/, 3 /*current_version*/, false, 2},
    // No current version in pref, no Finch parameter.
    {true, false, false, -1 /*version*/, -1 /*current_version*/, false, -1},
    // Has current version in pref, no Finch parameter.
    {true, false, false, -1 /*version*/, 10 /*current_version*/, false, 10},
};

TEST_P(AnnouncementNotificationServiceVersionTest, VersionTest) {
  const auto& param = GetParam();
  auto now = SetNow("10 Feb 2020 13:00:00");
  std::map<std::string, std::string> parameters = {
      {kSkipFirstRun, base::ToString(param.skip_first_run)},
      {kVersion, base::NumberToString(param.version)}};
  Init(parameters, param.enable_feature, false /*sign_in*/,
       param.current_version, false);

  ON_CALL(*delegate(), IsFirstRun()).WillByDefault(Return(param.is_first_run));
  EXPECT_CALL(*delegate(), ShowNotification())
      .Times(param.show_notification_called ? 1 : 0);
  service()->MaybeShowNotification();
  EXPECT_EQ(CurrentVersionPref(), param.expected_version_pref);
  EXPECT_EQ(FirstRunTimePref(), param.is_first_run ? now : base::Time());
}

INSTANTIATE_TEST_SUITE_P(All,
                         AnnouncementNotificationServiceVersionTest,
                         testing::ValuesIn(kVersionTestParams));

}  // namespace
