chromium/ash/system/scheduled_feature/scheduled_feature_unittest.cc

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

#include <cmath>
#include <limits>
#include <memory>
#include <sstream>
#include <string>
#include <utility>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/ash_prefs.h"
#include "ash/public/cpp/schedule_enums.h"
#include "ash/public/cpp/session/session_types.h"
#include "ash/session/session_controller_impl.h"
#include "ash/session/test_session_controller_client.h"
#include "ash/shell.h"
#include "ash/style/dark_light_mode_controller_impl.h"
#include "ash/system/geolocation/geolocation_controller.h"
#include "ash/system/geolocation/geolocation_controller_test_util.h"
#include "ash/system/geolocation/test_geolocation_url_loader_factory.h"
#include "ash/system/scheduled_feature/scheduled_feature.h"
#include "ash/system/time/time_of_day.h"
#include "ash/test/ash_test_base.h"
#include "ash/test/ash_test_helper.h"
#include "ash/test/failing_local_time_converter.h"
#include "ash/test/time_of_day_test_util.h"
#include "ash/test_shell_delegate.h"
#include "base/command_line.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/numerics/ranges.h"
#include "base/numerics/safe_conversions.h"
#include "base/scoped_observation.h"
#include "base/strings/pattern.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/vector3d_f.h"

namespace ash {

namespace {

using ::testing::_;
using ::testing::AtLeast;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::Mock;
using ::testing::Pair;
using RefreshReason = ScheduledFeature::RefreshReason;

constexpr char kUser1Email[] = "user1@featuredschedule";
constexpr char kUser2Email[] = "user2@featuredschedule";

constexpr char kTestEnabledPref[] = "ash.test.scheduled_feature.enabled";
constexpr char kTestScheduleTypePref[] =
    "ash.test.scheduled_feature.schedule_type";
constexpr char kTestCustomStartTimePref[] =
    "ash.test.scheduled_feature.custom_start_time";
constexpr char kTestCustomEndTimePref[] =
    "ash.test.scheduled_feature.custom_end_time";

// 6:00 PM
constexpr int kTestCustomStartTimeOffsetMinutes = 18 * 60;
// 6:00 AM
constexpr int kTestCustomEndTimeOffsetMinutes = 6 * 60;

// Maximum backoff time for refreshing the schedule when failures are
// encountered.
constexpr base::TimeDelta kMaxRefreshBackoff = base::Minutes(1);

enum AmPm { kAM, kPM };

// Returns the `ScheduleCheckpoint` that is expected to come next after
// `current_checkpoint` (sunrise, morning, late afternoon, sunset, sunrise,
// etc).
ScheduleCheckpoint GetNextExpectedCheckpoint(
    ScheduleCheckpoint current_checkpoint) {
  switch (current_checkpoint) {
    // Sunset to sunrise schedule type:
    case ScheduleCheckpoint::kSunset:
      return ScheduleCheckpoint::kSunrise;
    case ScheduleCheckpoint::kSunrise:
      return ScheduleCheckpoint::kMorning;
    case ScheduleCheckpoint::kMorning:
      return ScheduleCheckpoint::kLateAfternoon;
    case ScheduleCheckpoint::kLateAfternoon:
      return ScheduleCheckpoint::kSunset;

    // Custom schedule type:
    case ScheduleCheckpoint::kEnabled:
      return ScheduleCheckpoint::kEnabled;
    case ScheduleCheckpoint::kDisabled:
      return ScheduleCheckpoint::kDisabled;
  }
}

// Records all changes made to the feature state from the time that
// PrefChangeObserver is constructed (turned on at 2 PM, turned off at 5 PM,
// turned back on at 11 PM, etc). Allows tests to not only verify that the
// feature changed status at the appropriate times, but also that it did not
// change at an unintended time. For example, if the feature is expected to turn
// on at 2 PM and it's now 12 PM, we want to make sure that it did not turn off
// then back on again at some point in the middle (say 1 PM).
class PrefChangeObserver {
 public:
  PrefChangeObserver(PrefService* pref_service, const base::Clock* clock)
      : clock_(clock) {
    pref_registrar_.Init(pref_service);
    pref_registrar_.Add(
        kTestEnabledPref,
        base::BindRepeating(&PrefChangeObserver::OnEnabledPrefChanged,
                            base::Unretained(this)));
  }
  ~PrefChangeObserver() = default;

  // Elements appear in chronological order:
  // <Time of the status change, the status of the feature at the time>
  const std::vector<std::pair<TimeOfDay, bool>>& changes() const {
    return changes_;
  }

  void ClearHistory() { changes_.clear(); }

 private:
  void OnEnabledPrefChanged() {
    changes_.emplace_back(
        TimeOfDay::FromTime(clock_->Now()),
        pref_registrar_.prefs()->GetBoolean(kTestEnabledPref));
  }

  PrefChangeRegistrar pref_registrar_;
  const raw_ptr<const base::Clock> clock_;
  std::vector<std::pair<TimeOfDay, bool>> changes_;
};

class CheckpointObserver : public ScheduledFeature::CheckpointObserver {
 public:
  CheckpointObserver(ScheduledFeature* feature, const base::Clock* clock)
      : clock_(clock) {
    observation_.Observe(feature);
  }
  CheckpointObserver(const CheckpointObserver&) = delete;
  CheckpointObserver& operator=(const CheckpointObserver&) = delete;
  ~CheckpointObserver() override = default;

  // ScheduledFeature::CheckpointObserver:
  void OnCheckpointChanged(const ScheduledFeature* src,
                           ScheduleCheckpoint new_checkpoint) override {
    changes_.emplace_back(TimeOfDay::FromTime(clock_->Now()), new_checkpoint);
  }

  // Elements appear in chronological order:
  // <Time of the checkpoint change, the checkpoint received>
  const std::vector<std::pair<TimeOfDay, ScheduleCheckpoint>>& changes() const {
    return changes_;
  }

 private:
  base::ScopedObservation<ScheduledFeature,
                          ScheduledFeature::CheckpointObserver>
      observation_{this};
  const raw_ptr<const base::Clock> clock_;
  std::vector<std::pair<TimeOfDay, ScheduleCheckpoint>> changes_;
};

class TestScheduledFeature : public ScheduledFeature {
 public:
  TestScheduledFeature(const std::string prefs_path_enabled,
                       const std::string prefs_path_schedule_type,
                       const std::string prefs_path_custom_start_time,
                       const std::string prefs_path_custom_end_time)
      : ScheduledFeature(prefs_path_enabled,
                         prefs_path_schedule_type,
                         prefs_path_custom_start_time,
                         prefs_path_custom_end_time) {}
  TestScheduledFeature(const TestScheduledFeature& other) = delete;
  TestScheduledFeature& operator=(const TestScheduledFeature& rhs) = delete;
  ~TestScheduledFeature() override {}

  // ScheduledFeature:
  const char* GetFeatureName() const override { return "TestFeature"; }
  const char* GetScheduleTypeHistogramName() const override {
    return schedule_type_histogram_name_.c_str();
  }
  MOCK_METHOD(void, RefreshFeatureState, (RefreshReason reason), (override));

  void set_schedule_type_histogram_name(
      std::string schedule_type_histogram_name) {
    schedule_type_histogram_name_ = std::move(schedule_type_histogram_name);
  }

 private:
  std::string schedule_type_histogram_name_;
};

class ScheduledFeatureTest : public NoSessionAshTestBase,
                             public ScheduledFeature::Clock {
 public:
  ScheduledFeatureTest()
      : task_runner_(base::MakeRefCounted<base::TestMockTimeTaskRunner>()),
        task_runner_origin_ticks_(task_runner_->NowTicks()) {}
  ScheduledFeatureTest(const ScheduledFeatureTest& other) = delete;
  ScheduledFeatureTest& operator=(const ScheduledFeatureTest& rhs) = delete;
  ~ScheduledFeatureTest() override = default;

  PrefService* user1_pref_service() {
    return Shell::Get()->session_controller()->GetUserPrefServiceForUser(
        AccountId::FromUserEmail(kUser1Email));
  }

  PrefService* user2_pref_service() {
    return Shell::Get()->session_controller()->GetUserPrefServiceForUser(
        AccountId::FromUserEmail(kUser2Email));
  }

  TestScheduledFeature* feature() const { return feature_.get(); }
  GeolocationController* geolocation_controller() {
    return geolocation_controller_;
  }
  const base::OneShotTimer* timer_ptr() const { return timer_ptr_; }

  TestGeolocationUrlLoaderFactory* factory() const {
    CHECK(SimpleGeolocationProvider::GetInstance());
    return static_cast<TestGeolocationUrlLoaderFactory*>(
        SimpleGeolocationProvider::GetInstance()
            ->GetSharedURLLoaderFactoryForTesting());
  }

  // AshTestBase:
  void SetUp() override {
    NoSessionAshTestBase::SetUp();

    SetWallClockOrigin("23 Dec 2021 12:00:00");

    // Set the clock of geolocation controller to our test clock to control the
    // time now.
    geolocation_controller_ = ash::Shell::Get()->geolocation_controller();
    geolocation_controller()->SetClockForTesting(this);

    // Every feature that is auto scheduled by default needs to set test clock.
    // Otherwise the tests will fails `DCHECK_GE(start_time, now)` in
    // `ScheduledFeature::RefreshScheduleTimer()` when the new user session
    // is entered and `InitFromUserPrefs()` triggers `RefreshScheduleTimer()`.
    ash::Shell::Get()->dark_light_mode_controller()->SetClockForTesting(this);

    CreateTestUserSessions();

    // Simulate user 1 login.
    SimulateNewUserFirstLogin(kUser1Email);

    feature_ = std::make_unique<TestScheduledFeature>(
        kTestEnabledPref, kTestScheduleTypePref, kTestCustomStartTimePref,
        kTestCustomEndTimePref);
    ASSERT_FALSE(feature_->GetEnabled());

    feature_->SetClockForTesting(this);
    feature_->SetTaskRunnerForTesting(task_runner_);
    feature_->OnActiveUserPrefServiceChanged(
        Shell::Get()->session_controller()->GetActivePrefService());

    timer_ptr_ = geolocation_controller()->GetTimerForTesting();
  }

  void TearDown() override {
    feature_.reset();
    NoSessionAshTestBase::TearDown();
  }

  // ScheduledFeature::Clock:
  base::Time Now() const override {
    // Some tests may want to control the UTC wall clock time around which the
    // test is centered. `wall_clock_origin_` allows for this, and the value
    // returned here still reflects the advancement of `task_runner_`'s mock
    // time.
    const base::TimeDelta mock_time_elapsed =
        NowTicks() - task_runner_origin_ticks_;
    return wall_clock_origin_ + wall_clock_artificial_advancement_ +
           mock_time_elapsed;
  }

  base::TimeTicks NowTicks() const override { return task_runner_->NowTicks(); }

  void CreateTestUserSessions() {
    GetSessionControllerClient()->Reset();
    AddUserSession(kUser1Email);
    AddUserSession(kUser2Email);
  }

  void AddUserSession(const std::string& user_email) {
    auto prefs = std::make_unique<TestingPrefServiceSimple>();
    prefs->registry()->RegisterBooleanPref(kTestEnabledPref, false);
    prefs->registry()->RegisterIntegerPref(
        kTestScheduleTypePref, static_cast<int>(ScheduleType::kNone));
    prefs->registry()->RegisterIntegerPref(kTestCustomStartTimePref,
                                           kTestCustomStartTimeOffsetMinutes);
    prefs->registry()->RegisterIntegerPref(kTestCustomEndTimePref,
                                           kTestCustomEndTimeOffsetMinutes);
    RegisterUserProfilePrefs(prefs->registry(), /*country=*/"",
                             /*for_test=*/true);
    auto* const session_controller_client = GetSessionControllerClient();
    session_controller_client->AddUserSession(user_email,
                                              user_manager::UserType::kRegular,
                                              /*provide_pref_service=*/false);
    session_controller_client->SetUserPrefService(
        AccountId::FromUserEmail(user_email), std::move(prefs));
  }

  void SwitchActiveUser(const std::string& email) {
    GetSessionControllerClient()->SwitchActiveUser(
        AccountId::FromUserEmail(email));
  }

  bool GetEnabled() { return feature_->GetEnabled(); }
  ScheduleType GetScheduleType() { return feature_->GetScheduleType(); }

  void SetFeatureEnabled(bool enabled) { feature_->SetEnabled(enabled); }
  void SetScheduleType(ScheduleType type) { feature_->SetScheduleType(type); }

  // Convenience function for constructing a TimeOfDay object for exact hours
  // during the day. |hour| is between 1 and 12.
  TimeOfDay MakeTimeOfDay(int hour, AmPm am_pm) {
    DCHECK_GE(hour, 1);
    DCHECK_LE(hour, 12);

    if (am_pm == kAM) {
      hour %= 12;
    } else {
      if (hour != 12)
        hour += 12;
      hour %= 24;
    }

    return TimeOfDay(hour * 60).SetClock(this);
  }

  void FastForwardBy(base::TimeDelta amount) {
    task_runner_->FastForwardBy(amount);
  }

  // Fast forwards to the next point in time that the specified `time_of_day`
  // is hit. Examples:
  // 1) now = 2 PM time_of_day = 5 PM, advances 3 hours
  // 2) now = 7 PM time_of_day = 5 PM, advances 22 hours (the next day)
  void FastForwardTo(TimeOfDay time_of_day) {
    base::Time target_time = ToTimeToday(time_of_day.SetClock(this));
    const base::Time now = Now();
    if (target_time < now) {
      target_time += base::Days(1);
      ASSERT_GT(target_time, now);
    }
    FastForwardBy(target_time - now);
  }

  // To simulate events like the device suspending (wall clock advances, but
  // clock "ticks" don't).
  void AdvanceTimeBy(base::TimeDelta amount) {
    wall_clock_artificial_advancement_ += amount;
  }

  // Fires the timer of the scheduler to request geoposition and wait for all
  // observers to receive the latest geoposition from the server.
  void FireTimerToFetchGeoposition() {
    GeopositionResponsesWaiter waiter(geolocation_controller_);
    EXPECT_TRUE(timer_ptr()->IsRunning());
    // Fast forward the scheduler to reach the time when the controller
    // requests for geoposition from the server in
    // `GeolocationController::RequestGeoposition`.
    timer_ptr_->FireNow();
    // Waits for the observers to receive the geoposition from the server.
    waiter.Wait();
  }

  // Checks if the feature is observing geoposition changes.
  bool IsFeatureObservingGeoposition() {
    return geolocation_controller()->HasObserver(feature());
  }

  // Sets the wall clock time at which the test case starts. After calling this,
  // future calls to Now() will return `utc_time_str` plus the amount of mock
  // time that has elapsed thus far in the test. See
  // `base::Time::FromUTCString()` for format of `utc_time_str`.
  void SetWallClockOrigin(const char* const utc_time_str) {
    ASSERT_TRUE(base::Time::FromUTCString(utc_time_str, &wall_clock_origin_));
  }

  // Simulates scenarios where the code is receiving valid `base::Time` values
  // from the clock, but converting them to/from local time is failing.
  void SetLocalTimeConverter(const LocalTimeConverter* local_time_converter) {
    geolocation_controller()->SetLocalTimeConverterForTesting(
        local_time_converter);
    ash::Shell::Get()
        ->dark_light_mode_controller()
        ->SetLocalTimeConverterForTesting(local_time_converter);
    feature_->SetLocalTimeConverterForTesting(local_time_converter);
  }

 private:
  // It is infeasible to initialize AshTestBase with "mock" time and fast
  // forward the mock TaskEnvironment in these test cases. Why: The
  // AshTestBase harness also instantiates a large portion of the UI stack
  // (even though it's irrelevant to these tests), which causes
  // TaskEnvironment::FastForwardBy() to block for long periods of time. The
  // test cases ultimately take too long.
  //
  // To solve, create a dedicated `base::TestMockTimeTaskRunner` just for
  // `ScheduledFeature` and its dependencies. The `task_runner_` is still
  // run on the main test thread.
  const scoped_refptr<base::TestMockTimeTaskRunner> task_runner_;
  // The time at which `task_runner_`'s internal mock tick clock starts.
  const base::TimeTicks task_runner_origin_ticks_;
  // Wall clock time at which the test case starts. Individual test cases may
  // adjust this with the `SetWallClockOrigin()` method. This is reflected in
  // the return value of `Now()`.
  base::Time wall_clock_origin_;
  // Used to support `AdvanceTimeBy()`, where the wall clock is advanced but
  // the tick clock is not (simulates a device suspending). This is reflected
  // in the return value of `Now()` and defaults to 0.
  base::TimeDelta wall_clock_artificial_advancement_;
  std::unique_ptr<TestScheduledFeature> feature_;
  raw_ptr<GeolocationController, DanglingUntriaged> geolocation_controller_;
  raw_ptr<base::OneShotTimer, DanglingUntriaged> timer_ptr_;
  Geoposition position_;
};

struct TestTimestamp {
  // Passed to `base::Time::FromUTCString()`.
  const char* utc_value = nullptr;
  // Human-readable label printed in test output.
  const char* label = nullptr;
};

struct TimeAndLocation {
  TestTimestamp timestamp;
  SimpleGeoposition geoposition;
};

// Iterates through all possible geopositions using the `kSunsetToSunrise`
// schedule type. Gives comprehensive test coverage for all seasons and for all
// parts of the globe.
class ScheduledFeatureGeopositionTest
    : public ScheduledFeatureTest,
      public testing::WithParamInterface<TimeAndLocation> {
 public:
  static constexpr TestTimestamp kAllTimestamps[] = {
      {"07 Jan 2023 20:30:00.000", "Winter"},
      {"07 Apr 2023 20:30:00.000", "Spring"},
      {"07 Jun 2023 20:30:00.000", "Summer"},
      {"07 Oct 2023 20:30:00.000", "Fall"},
  };

  // Generates coordinates in range ([-90, +90], [-180, +180]) with 15 degree
  // step size in latitude and 30 degree step size in longitude.
  static std::vector<TimeAndLocation> GenerateTestParams() {
    static constexpr double kMinLatitude = -90.f;
    static constexpr double kMaxLatitude = 90.f;
    static constexpr double kMinLongitude = -180.f;
    static constexpr double kMaxLongitude = 180.f;
    static constexpr double kLatitudeStepSize = 15.f;
    static constexpr double kLongitudeStepSize = 30.f;

    // Accounts for precision lost when incrementing latitudes/longitudes. Ex:
    // 150.f + 30.f may not necessarily equal 180.f exactly. If it evaluates to
    // something slightly greater than 180.f, the geoposition code will consider
    // this an invalid longitude and skip it. This ensures we are testing the
    // max values of the lat/long ranges.
    const auto increment_coordinate =
        [](const double max_value, const double step_size, double& coordinate) {
          constexpr double kCoordinateEpsilon = 0.001;
          coordinate += step_size;
          if (base::IsApproximatelyEqual(coordinate, max_value,
                                         kCoordinateEpsilon)) {
            coordinate = max_value;
          }
        };

    std::vector<TimeAndLocation> test_params;
    for (const TestTimestamp& timestamp : kAllTimestamps) {
      TimeAndLocation time_and_location;
      time_and_location.timestamp = timestamp;

      for (double latitude = kMinLatitude; latitude <= kMaxLatitude;
           increment_coordinate(kMaxLatitude, kLatitudeStepSize, latitude)) {
        for (double longitude = kMinLongitude; longitude <= kMaxLongitude;
             increment_coordinate(kMaxLongitude, kLongitudeStepSize,
                                  longitude)) {
          time_and_location.geoposition = {latitude, longitude};
          test_params.push_back(time_and_location);
        }
      }
    }
    return test_params;
  }

  void SetUp() override {
    ScheduledFeatureTest::SetUp();
    SetWallClockOrigin(GetParam().timestamp.utc_value);
    factory()->SetValidPosition(GetParam().geoposition.latitude,
                                GetParam().geoposition.longitude, Now());
    FireTimerToFetchGeoposition();
    feature()->SetScheduleType(ScheduleType::kSunsetToSunrise);
  }
};

INSTANTIATE_TEST_SUITE_P(
    AllGeopositions,
    ScheduledFeatureGeopositionTest,
    testing::ValuesIn(ScheduledFeatureGeopositionTest::GenerateTestParams()),
    [](const testing::TestParamInfo<ScheduledFeatureGeopositionTest::ParamType>&
           info) {
      // gtest only permits alphanumeric characters in the generated name.
      const auto coordinate_to_string = [](double coordinate) {
        std::string coordinate_str =
            base::NumberToString(base::ClampRound(coordinate));
        base::ReplaceChars(coordinate_str, "-", "Negative", &coordinate_str);
        return coordinate_str;
      };
      return base::StringPrintf(
          "%sLat%sLong%s", info.param.timestamp.label,
          coordinate_to_string(info.param.geoposition.latitude).c_str(),
          coordinate_to_string(info.param.geoposition.longitude).c_str());
    });

// Tests that switching users retrieves the feature settings for the active
// user's prefs.
TEST_F(ScheduledFeatureTest, UserSwitchAndSettingsPersistence) {
  // Start with user1 logged in and update to sunset-to-sunrise schedule type.
  const std::string kScheduleTypePrefString = kTestScheduleTypePref;
  constexpr ScheduleType kUser1ScheduleType = ScheduleType::kSunsetToSunrise;
  constexpr bool kUser1EnabledState = false;
  constexpr ScheduleCheckpoint kUser1Checkpoint = ScheduleCheckpoint::kMorning;
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kSettingsChanged));
  feature()->SetScheduleType(kUser1ScheduleType);
  Mock::VerifyAndClearExpectations(feature());
  FastForwardTo(MakeTimeOfDay(10, AmPm::kAM));
  EXPECT_EQ(GetScheduleType(), kUser1ScheduleType);
  EXPECT_EQ(user1_pref_service()->GetInteger(kScheduleTypePrefString),
            static_cast<int>(kUser1ScheduleType));
  EXPECT_EQ(GetEnabled(), kUser1EnabledState);
  EXPECT_EQ(feature()->current_checkpoint(), kUser1Checkpoint);

  // Switch to user 2, and set to custom schedule type.
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kSettingsChanged));
  SwitchActiveUser(kUser2Email);
  Mock::VerifyAndClearExpectations(feature());

  const ScheduleType user2_schedule_type = ScheduleType::kCustom;
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kSettingsChanged));
  user2_pref_service()->SetInteger(kScheduleTypePrefString,
                                   static_cast<int>(user2_schedule_type));
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_EQ(GetScheduleType(), user2_schedule_type);
  EXPECT_EQ(user2_pref_service()->GetInteger(kScheduleTypePrefString),
            static_cast<int>(user2_schedule_type));

  // Switch back to user 1, to find feature schedule type is restored to
  // sunset-to-sunrise with the correct enabled state and checkpoint.
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kSettingsChanged));
  SwitchActiveUser(kUser1Email);
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_EQ(GetScheduleType(), kUser1ScheduleType);
  EXPECT_EQ(GetEnabled(), kUser1EnabledState);
  EXPECT_EQ(feature()->current_checkpoint(), kUser1Checkpoint);
}

// Tests that the scheduler type is initiallized from user prefs and observes
// geoposition when the scheduler is enabled.
TEST_F(ScheduledFeatureTest, InitScheduleTypeFromUserPrefs) {
  // Start with user1 logged in with the default disabled scheduler, `kNone`.
  const std::string kScheduleTypePrefString = kTestScheduleTypePref;
  const ScheduleType user1_schedule_type = ScheduleType::kNone;
  EXPECT_EQ(user1_schedule_type, GetScheduleType());
  // Check that the feature does not observe the geoposition when the schedule
  // type is `kNone`.
  EXPECT_FALSE(IsFeatureObservingGeoposition());

  // Update user2's schedule type pref to sunset-to-sunrise.
  const ScheduleType user2_schedule_type = ScheduleType::kSunsetToSunrise;
  user2_pref_service()->SetInteger(kScheduleTypePrefString,
                                   static_cast<int>(user2_schedule_type));
  // Switching to user2 should update the schedule type to sunset-to-sunrise.
  SwitchActiveUser(kUser2Email);
  EXPECT_EQ(user2_schedule_type, GetScheduleType());
  // Check that the feature starts observing geoposition when the schedule
  // type is changed to `kSunsetToSunrise`.
  EXPECT_TRUE(IsFeatureObservingGeoposition());

  // Set custom schedule to test that once we switch to the user1 with `kNone`
  // schedule type, the feature should remove itself from a geolocation
  // observer.
  feature()->SetScheduleType(ScheduleType::kCustom);
  EXPECT_TRUE(IsFeatureObservingGeoposition());
  // Make sure that switching back to user1 makes it remove itself from the
  // geoposition observer.
  SwitchActiveUser(kUser1Email);
  EXPECT_EQ(user1_schedule_type, GetScheduleType());
  EXPECT_FALSE(IsFeatureObservingGeoposition());
}

// Tests transitioning from kNone to kCustom and back to kNone schedule
// types.
TEST_F(ScheduledFeatureTest, ScheduleNoneToCustomTransition) {
  // Now is 6:00 PM.
  FastForwardTo(MakeTimeOfDay(6, AmPm::kPM));
  SetFeatureEnabled(false);
  feature()->SetScheduleType(ScheduleType::kNone);
  // Start time is at 3:00 PM and end time is at 8:00 PM.
  feature()->SetCustomStartTime(MakeTimeOfDay(3, AmPm::kPM));
  feature()->SetCustomEndTime(MakeTimeOfDay(8, AmPm::kPM));

  //      15:00         18:00         20:00
  // <----- + ----------- + ----------- + ----->
  //        |             |             |
  //      start          now           end
  //
  // Even though "Now" is inside the feature interval, nothing should
  // change, since the schedule type is "none".
  EXPECT_FALSE(GetEnabled());

  // Now change the schedule type to custom, the feature should turn on
  // immediately, and the timer should be running with a delay of exactly 2
  // hours scheduling the end.
  feature()->SetScheduleType(ScheduleType::kCustom);
  EXPECT_TRUE(GetEnabled());
  PrefChangeObserver change_log(user1_pref_service(), this);
  FastForwardTo(MakeTimeOfDay(8, AmPm::kPM));
  EXPECT_FALSE(GetEnabled());
  EXPECT_THAT(change_log.changes(),
              ElementsAre(Pair(MakeTimeOfDay(8, AmPm::kPM), false)));

  // First reset back to 6 p.m. the next day:
  FastForwardTo(MakeTimeOfDay(6, AmPm::kPM));
  ASSERT_TRUE(GetEnabled());

  // If the user changes the schedule type to "none", the feature status
  // should not change.
  feature()->SetScheduleType(ScheduleType::kNone);
  EXPECT_TRUE(GetEnabled());
  // Since schedule type is none, the feature should not switch off at the
  // scheduled end.
  FastForwardTo(MakeTimeOfDay(8, AmPm::kPM));
  EXPECT_TRUE(GetEnabled());
}

// Tests what happens when the time now reaches the end of the feature
// interval when the feature mode is on.
TEST_F(ScheduledFeatureTest, TestCustomScheduleReachingEndTime) {
  FastForwardTo(MakeTimeOfDay(6, AmPm::kPM));
  feature()->SetCustomStartTime(MakeTimeOfDay(3, AmPm::kPM));
  feature()->SetCustomEndTime(MakeTimeOfDay(8, AmPm::kPM));
  feature()->SetScheduleType(ScheduleType::kCustom);
  EXPECT_TRUE(GetEnabled());

  PrefChangeObserver change_log(user1_pref_service(), this);

  // Simulate reaching the end time by triggering the timer's user task. Make
  // sure that the feature ended.
  //
  //      15:00                      20:00
  // <----- + ------------------------ + ----->
  //        |                          |
  //      start                    end & now
  //
  // Now is 8:00 PM.
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kScheduled));
  FastForwardTo(MakeTimeOfDay(8, AmPm::kPM));
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_FALSE(GetEnabled());
  // The feature should be scheduled to start again at 3:00 PM tomorrow.
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kScheduled));
  FastForwardTo(MakeTimeOfDay(3, AmPm::kPM));
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_THAT(change_log.changes(),
              ElementsAre(Pair(MakeTimeOfDay(8, AmPm::kPM), false),
                          Pair(MakeTimeOfDay(3, AmPm::kPM), true)));
}

// Tests that user toggles from the system menu or system settings override any
// status set by an automatic schedule.
TEST_F(ScheduledFeatureTest, ExplicitUserTogglesWhileScheduleIsActive) {
  // Start with the below custom schedule, where the feature is off.
  //
  //      15:00               20:00          23:00
  // <----- + ----------------- + ------------ + ---->
  //        |                   |              |
  //      start                end            now
  //
  FastForwardTo(MakeTimeOfDay(11, AmPm::kPM));
  feature()->SetCustomStartTime(MakeTimeOfDay(3, AmPm::kPM));
  feature()->SetCustomEndTime(MakeTimeOfDay(8, AmPm::kPM));
  feature()->SetScheduleType(ScheduleType::kCustom);
  EXPECT_FALSE(GetEnabled());

  // What happens if the user manually turns the feature on while the schedule
  // type says it should be off?
  // User toggles either from the system menu or the System Settings toggle
  // button must override any automatic schedule.
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kExternal));
  SetFeatureEnabled(true);
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_TRUE(GetEnabled());

  PrefChangeObserver change_log(user1_pref_service(), this);
  // The feature should automatically turn off at 8:00 PM tomorrow. May refresh
  // at 3:00 PM with no change in status.
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kScheduled))
      .Times(AtLeast(1));
  FastForwardTo(MakeTimeOfDay(8, AmPm::kPM));
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_FALSE(GetEnabled());

  // Manually reset the feature back to on.
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kExternal));
  SetFeatureEnabled(true);
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_TRUE(GetEnabled());

  // Manually turning it back off should also be respected, and this time the
  // start is scheduled at 3:00 PM tomorrow after 19 hours from "now" (8:00
  // PM).
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kExternal));
  SetFeatureEnabled(false);
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_FALSE(GetEnabled());
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kScheduled));
  FastForwardTo(MakeTimeOfDay(3, AmPm::kPM));
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_TRUE(GetEnabled());
  EXPECT_THAT(
      change_log.changes(),
      ElementsAre(
          Pair(MakeTimeOfDay(8, AmPm::kPM), false),
          Pair(MakeTimeOfDay(8, AmPm::kPM), true),   // Manual toggle on
          Pair(MakeTimeOfDay(8, AmPm::kPM), false),  // Manual toggle off
          Pair(MakeTimeOfDay(3, AmPm::kPM), true)));
}

// Tests that changing the custom start and end times, in such a way that
// shouldn't change the current status, only updates the timer but doesn't
// change the status.
// TODO(crbug.com/40889492): Fix test failure and re-enable on ChromeOS.
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_ChangingStartTimesThatDontChangeTheStatus \
  DISABLED_ChangingStartTimesThatDontChangeTheStatus
#else
#define MAYBE_ChangingStartTimesThatDontChangeTheStatus \
  ChangingStartTimesThatDontChangeTheStatus
#endif
TEST_F(ScheduledFeatureTest, MAYBE_ChangingStartTimesThatDontChangeTheStatus) {
  //       16:00        18:00         22:00
  // <----- + ----------- + ----------- + ----->
  //        |             |             |
  //       now          start          end
  //
  FastForwardTo(MakeTimeOfDay(4, AmPm::kPM));  // 4:00 PM.
  SetFeatureEnabled(false);
  feature()->SetScheduleType(ScheduleType::kNone);
  feature()->SetCustomStartTime(MakeTimeOfDay(6, AmPm::kPM));  // 6:00 PM.
  feature()->SetCustomEndTime(MakeTimeOfDay(10, AmPm::kPM));   // 10:00 PM.

  // Since now is outside the feature interval, changing the schedule type
  // to kCustom, shouldn't affect the status. Validate the feature turns on then
  // off at the scheduled times.
  feature()->SetScheduleType(ScheduleType::kCustom);
  EXPECT_FALSE(GetEnabled());
  PrefChangeObserver change_log(user1_pref_service(), this);
  FastForwardBy(base::Days(1));
  EXPECT_THAT(change_log.changes(),
              ElementsAre(Pair(MakeTimeOfDay(6, AmPm::kPM), true),
                          Pair(MakeTimeOfDay(10, AmPm::kPM), false)));

  change_log.ClearHistory();
  // Change the start time in such a way that doesn't change the status, but
  // despite that, confirm that schedule has been updated.
  ASSERT_FALSE(GetEnabled());
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kSettingsChanged));
  feature()->SetCustomStartTime(MakeTimeOfDay(7, AmPm::kPM));  // 7:00 PM.
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_FALSE(GetEnabled());

  // Changing the end time in a similar fashion to the above and expect no
  // change.
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kSettingsChanged));
  feature()->SetCustomEndTime(MakeTimeOfDay(11, AmPm::kPM));  // 11:00 PM.
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_FALSE(GetEnabled());
  FastForwardBy(base::Days(1));
  EXPECT_THAT(change_log.changes(),
              ElementsAre(Pair(MakeTimeOfDay(7, AmPm::kPM), true),
                          Pair(MakeTimeOfDay(11, AmPm::kPM), false)));
}

// Tests that the feature should turn on at sunset time and turn off at sunrise
// time.
TEST_F(ScheduledFeatureTest, SunsetSunrise) {
  EXPECT_FALSE(GetEnabled());
  EXPECT_EQ(feature()->current_checkpoint(), ScheduleCheckpoint::kDisabled);

  // Set time now to 10:00 AM.
  FastForwardTo(MakeTimeOfDay(10, AmPm::kAM));
  const CheckpointObserver checkpoint_observer(feature(), this);
  feature()->SetScheduleType(ScheduleType::kSunsetToSunrise);
  EXPECT_FALSE(GetEnabled());

  const PrefChangeObserver change_log(user1_pref_service(), this);

  // Set time now to 4:00 PM.
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kScheduled));
  FastForwardTo(MakeTimeOfDay(4, AmPm::kPM));
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_FALSE(GetEnabled());

  // Firing a timer should to advance the time to sunset and automatically turn
  // on the feature.
  const auto sunset = geolocation_controller()->GetSunsetTime();
  ASSERT_TRUE(sunset.has_value());
  const TimeOfDay sunset_time = TimeOfDay::FromTime(sunset.value());
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kScheduled));
  FastForwardTo(sunset_time);
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_TRUE(GetEnabled());

  // Firing a timer should advance the time to sunrise and automatically turn
  // off the feature.
  const auto sunrise = geolocation_controller()->GetSunriseTime();
  ASSERT_TRUE(sunrise.has_value());
  const TimeOfDay sunrise_time = TimeOfDay::FromTime(sunrise.value());
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kScheduled));
  FastForwardTo(sunrise_time);
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_FALSE(GetEnabled());

  EXPECT_THAT(change_log.changes(),
              ElementsAre(Pair(sunset_time, true), Pair(sunrise_time, false)));
  EXPECT_THAT(
      checkpoint_observer.changes(),
      ElementsAre(
          Pair(MakeTimeOfDay(10, AmPm::kAM), ScheduleCheckpoint::kMorning),
          Pair(MakeTimeOfDay(4, AmPm::kPM), ScheduleCheckpoint::kLateAfternoon),
          Pair(sunset_time, ScheduleCheckpoint::kSunset),
          Pair(sunrise_time, ScheduleCheckpoint::kSunrise)));
}

// Tests that scheduled start time and end time of sunset-to-sunrise feature
// are updated correctly if the geoposition changes.
TEST_F(ScheduledFeatureTest, SunsetSunriseGeoposition) {
  constexpr double kFakePosition1_Latitude = 23.5;
  constexpr double kFakePosition1_Longitude = 55.88;
  constexpr double kFakePosition2_Latitude = 23.5;
  constexpr double kFakePosition2_Longitude = 10.9;
  // Position 1 sunset and sunrise times.
  //
  //    sunset-4
  // <----- + --------- + ---------------- + ------->
  //        |           |                  |
  //       now        sunset            sunrise
  //

  GeolocationControllerObserver observer1;
  geolocation_controller()->AddObserver(&observer1);
  EXPECT_TRUE(timer_ptr()->IsRunning());
  EXPECT_FALSE(observer1.possible_change_in_timezone());

  // Set and fetch position update.
  factory()->SetValidPosition(kFakePosition1_Latitude, kFakePosition1_Longitude,
                              Now());
  FireTimerToFetchGeoposition();
  EXPECT_TRUE(observer1.possible_change_in_timezone());
  const auto sunset_time1 = geolocation_controller()->GetSunsetTime();
  const auto sunrise_time1 = geolocation_controller()->GetSunriseTime();
  ASSERT_TRUE(sunset_time1.has_value());
  ASSERT_TRUE(sunrise_time1.has_value());
  // Our assumption is that GeolocationController gives us sunrise time
  // earlier in the same day before sunset.
  ASSERT_GT(sunset_time1.value(), sunrise_time1.value());
  ASSERT_LT(sunset_time1.value() - base::Days(1), sunrise_time1.value());

  // Set time now to be 4 hours before sunset.
  FastForwardTo(TimeOfDay::FromTime(sunset_time1.value() - base::Hours(4)));

  // Expect that timer is running and the start is scheduled after 4 hours.
  EXPECT_FALSE(feature()->GetEnabled());

  feature()->SetScheduleType(ScheduleType::kSunsetToSunrise);
  EXPECT_FALSE(feature()->GetEnabled());
  EXPECT_TRUE(IsFeatureObservingGeoposition());

  // A small delta used to help forwarding the time to be a little bit behind
  // the target time. Used to avoid test flaky because of the time issue.
  const base::TimeDelta delta = base::Minutes(5);
  // Simulate reaching sunset.
  FastForwardBy(base::Hours(4) +
                delta);  // Now is sunset time of the position1.
  EXPECT_TRUE(feature()->GetEnabled());

  // Simulate reaching sunrise.
  FastForwardTo(TimeOfDay::FromTime(
      sunrise_time1.value() + delta));  // Now is sunrise time of the position1
  EXPECT_FALSE(feature()->GetEnabled());

  // Now simulate user changing position.
  // Position 2 sunset and sunrise times.
  //
  // <----- + --------- + ---------------- + ------->
  //        |           |                  |
  //      sunset2      now (sunrise1)     sunrise2
  //

  // Replace a response `position` with `position2`.
  factory()->ClearResponses();
  factory()->SetValidPosition(kFakePosition2_Latitude, kFakePosition2_Longitude,
                              Now());
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kReset));
  FireTimerToFetchGeoposition();
  Mock::VerifyAndClearExpectations(feature());
  EXPECT_TRUE(observer1.possible_change_in_timezone());
  EXPECT_TRUE(IsFeatureObservingGeoposition());

  const auto sunset_time2 = geolocation_controller()->GetSunsetTime();
  const auto sunrise_time2 = geolocation_controller()->GetSunriseTime();
  ASSERT_TRUE(sunset_time2.has_value());
  ASSERT_TRUE(sunrise_time2.has_value());

  // Expect that the scheduled end delay has been updated to sunrise of location
  // 2, and the status has changed to enabled even though time has not advanced.
  EXPECT_TRUE(feature()->GetEnabled());

  // Simulate reaching sunrise.
  FastForwardTo(TimeOfDay::FromTime(
      sunrise_time2.value() + delta));  // Now is sunrise time of the position2.
  EXPECT_FALSE(feature()->GetEnabled());
  // Timer is running scheduling the start at the sunset of the next day.
  FastForwardTo(TimeOfDay::FromTime(sunset_time2.value() + delta));
  EXPECT_TRUE(feature()->GetEnabled());
}

// Tests that the feature is disabled and there are no crashes/unpredictable
// behavior if there is 24 hours of daylight.
TEST_F(ScheduledFeatureTest, SunsetSunriseAllDaylight) {
  // 24 hours of daylight (Kiruna, Sweden)
  constexpr double kTestLatitude = 67.855800;
  constexpr double kTestLongitude = 20.225282;

  SetWallClockOrigin("07 Jun 2023 20:30:00.000");

  // Set and fetch position update.
  factory()->SetValidPosition(kTestLatitude, kTestLongitude, Now());
  FireTimerToFetchGeoposition();

  feature()->SetScheduleType(ScheduleType::kSunsetToSunrise);
  EXPECT_FALSE(feature()->GetEnabled());
  FastForwardBy(base::Days(1));
  EXPECT_FALSE(feature()->GetEnabled());
}

// Tests that on device resume from sleep, the feature status is updated
// correctly if the time has changed meanwhile.
TEST_F(ScheduledFeatureTest, CustomScheduleOnResume) {
  // Now is 4:00 PM.
  FastForwardTo(MakeTimeOfDay(4, AmPm::kPM));
  feature()->SetEnabled(false);
  // Start time is at 6:00 PM and end time is at 10:00 PM. The feature should be
  // off.
  //      16:00         18:00         22:00
  // <----- + ----------- + ----------- + ----->
  //        |             |             |
  //       now          start          end
  //
  feature()->SetCustomStartTime(MakeTimeOfDay(6, AmPm::kPM));
  feature()->SetCustomEndTime(MakeTimeOfDay(10, AmPm::kPM));
  feature()->SetScheduleType(ScheduleType::kCustom);
  ASSERT_FALSE(feature()->GetEnabled());

  PrefChangeObserver change_log(user1_pref_service(), this);

  // Now simulate that the device was suspended for 3 hours, and the time now
  // is 7:00 PM when the devices was resumed. Expect that the feature turns on.
  AdvanceTimeBy(base::Hours(3));  // 7:00 PM
  EXPECT_CALL(*feature(), RefreshFeatureState(RefreshReason::kReset));
  feature()->SuspendDone(base::TimeDelta::Max());
  Mock::VerifyAndClearExpectations(feature());

  EXPECT_TRUE(feature()->GetEnabled());
  // The feature should be disabled at originally scheduled time.
  FastForwardTo(MakeTimeOfDay(10, AmPm::kPM));
  EXPECT_FALSE(feature()->GetEnabled());

  EXPECT_THAT(change_log.changes(),
              ElementsAre(Pair(MakeTimeOfDay(7, AmPm::kPM), true),
                          Pair(MakeTimeOfDay(10, AmPm::kPM), false)));
}

// The following tests ensure that the feature schedule is correctly
// refreshed when the start and end times are inverted (i.e. the "start time" as
// a time of day today is in the future with respect to the "end time" also as a
// time of day today).
//
// Case 1: "Now" is less than both "end" and "start".
TEST_F(ScheduledFeatureTest, CustomScheduleInvertedStartAndEndTimesCase1) {
  // Now is 4:00 AM.
  FastForwardTo(MakeTimeOfDay(4, AmPm::kAM));
  SetFeatureEnabled(false);
  // Start time is at 9:00 PM and end time is at 6:00 AM. "Now" is less than
  // both. The feature should be on.
  //       4:00          6:00         21:00
  // <----- + ----------- + ----------- + ----->
  //        |             |             |
  //       now           end          start
  //
  feature()->SetCustomStartTime(MakeTimeOfDay(9, AmPm::kPM));
  feature()->SetCustomEndTime(MakeTimeOfDay(6, AmPm::kAM));
  feature()->SetScheduleType(ScheduleType::kCustom);

  EXPECT_TRUE(GetEnabled());
  PrefChangeObserver change_log(user1_pref_service(), this);
  FastForwardBy(base::Days(1));
  EXPECT_THAT(change_log.changes(),
              ElementsAre(Pair(MakeTimeOfDay(6, AmPm::kAM), false),
                          Pair(MakeTimeOfDay(9, AmPm::kPM), true)));
}

// Case 2: "Now" is between "end" and "start".
TEST_F(ScheduledFeatureTest, CustomScheduleInvertedStartAndEndTimesCase2) {
  // Now is 6:00 AM.
  FastForwardTo(MakeTimeOfDay(6, AmPm::kAM));
  SetFeatureEnabled(false);
  // Start time is at 9:00 PM and end time is at 4:00 AM. "Now" is between both.
  // The feature should be off.
  //       4:00          6:00         21:00
  // <----- + ----------- + ----------- + ----->
  //        |             |             |
  //       end           now          start
  //
  feature()->SetCustomStartTime(MakeTimeOfDay(9, AmPm::kPM));
  feature()->SetCustomEndTime(MakeTimeOfDay(4, AmPm::kAM));
  feature()->SetScheduleType(ScheduleType::kCustom);

  EXPECT_FALSE(GetEnabled());
  PrefChangeObserver change_log(user1_pref_service(), this);
  FastForwardBy(base::Days(1));
  EXPECT_THAT(change_log.changes(),
              ElementsAre(Pair(MakeTimeOfDay(9, AmPm::kPM), true),
                          Pair(MakeTimeOfDay(4, AmPm::kAM), false)));
}

// Case 3: "Now" is greater than both "start" and "end".
TEST_F(ScheduledFeatureTest, CustomScheduleInvertedStartAndEndTimesCase3) {
  // Now is 11:00 PM.
  FastForwardTo(MakeTimeOfDay(11, AmPm::kPM));
  SetFeatureEnabled(false);
  // Start time is at 9:00 PM and end time is at 4:00 AM. "Now" is greater than
  // both. the feature should be on.
  //       4:00         21:00         23:00
  // <----- + ----------- + ----------- + ----->
  //        |             |             |
  //       end          start          now
  //
  feature()->SetCustomStartTime(MakeTimeOfDay(9, AmPm::kPM));
  feature()->SetCustomEndTime(MakeTimeOfDay(4, AmPm::kAM));
  feature()->SetScheduleType(ScheduleType::kCustom);

  EXPECT_TRUE(GetEnabled());
  PrefChangeObserver change_log(user1_pref_service(), this);
  FastForwardBy(base::Days(1));
  EXPECT_THAT(change_log.changes(),
              ElementsAre(Pair(MakeTimeOfDay(4, AmPm::kAM), false),
                          Pair(MakeTimeOfDay(9, AmPm::kPM), true)));
}

// Tests that manual changes to the feature status while a schedule is being
// used will be remembered and reapplied across user switches.
TEST_F(ScheduledFeatureTest, MultiUserManualStatusToggleWithSchedules) {
  // Setup user 1 to use a custom schedule from 3pm till 8pm, and user 2 to use
  // a sunset-to-sunrise schedule from 6pm till 6am.
  //
  //
  //          |<--- User 1 NL on --->|
  //          |                      |
  // <--------+-------------+--------+----------------------------+----------->
  //         3pm           6pm      8pm                          6am
  //                        |                                     |
  //                        |<----------- User 2 NL on ---------->|
  //
  // Test cases at:
  //
  // <---+---------+------------+------------+----------------------------+--->
  //    2pm       4pm         7pm           10pm                         9am
  //

  FastForwardTo(MakeTimeOfDay(2, kPM));
  feature()->SetCustomStartTime(MakeTimeOfDay(3, kPM));
  feature()->SetCustomEndTime(MakeTimeOfDay(8, kPM));
  feature()->SetScheduleType(ScheduleType::kCustom);
  SwitchActiveUser(kUser2Email);
  feature()->SetScheduleType(ScheduleType::kSunsetToSunrise);
  SwitchActiveUser(kUser1Email);

  struct {
    TimeOfDay fake_now;
    bool user_1_expected_status;
    bool user_2_expected_status;
  } kTestCases[] = {
      {MakeTimeOfDay(2, kPM), false, false},
      {MakeTimeOfDay(4, kPM), true, false},
      {MakeTimeOfDay(7, kPM), true, true},
      {MakeTimeOfDay(10, kPM), false, true},
      {MakeTimeOfDay(9, kAM),  // 9:00 AM tomorrow.
       false, false},
  };

  for (const auto& test_case : kTestCases) {
    // Each test case begins when user_1 is active.
    const bool user_1_toggled_status = !test_case.user_1_expected_status;
    const bool user_2_toggled_status = !test_case.user_2_expected_status;

    // Apply the test's case fake time, and fire the timer if there's a change
    // expected in the feature's status.
    FastForwardTo(test_case.fake_now);

    // The untoggled states for both users should match the expected ones
    // according to their schedules.
    EXPECT_EQ(test_case.user_1_expected_status, feature()->GetEnabled());
    SwitchActiveUser(kUser2Email);
    EXPECT_EQ(test_case.user_2_expected_status, feature()->GetEnabled());

    // Manually toggle the feature for user_2 and expect that it will be
    // remembered when we switch to user_1 and back.
    feature()->SetEnabled(user_2_toggled_status);
    EXPECT_EQ(user_2_toggled_status, feature()->GetEnabled());
    SwitchActiveUser(kUser1Email);
    EXPECT_EQ(test_case.user_1_expected_status, feature()->GetEnabled());
    SwitchActiveUser(kUser2Email);
    EXPECT_EQ(user_2_toggled_status, feature()->GetEnabled());

    // Toggle it for user_1 as well, and expect it will be remembered and won't
    // affect the already toggled state for user_2.
    SwitchActiveUser(kUser1Email);
    EXPECT_EQ(test_case.user_1_expected_status, feature()->GetEnabled());
    feature()->SetEnabled(user_1_toggled_status);
    EXPECT_EQ(user_1_toggled_status, feature()->GetEnabled());
    SwitchActiveUser(kUser2Email);
    EXPECT_EQ(user_2_toggled_status, feature()->GetEnabled());

    // Toggle both users back to their original states in preparation for the
    // next test case.
    feature()->SetEnabled(test_case.user_2_expected_status);
    EXPECT_EQ(test_case.user_2_expected_status, feature()->GetEnabled());
    SwitchActiveUser(kUser1Email);
    EXPECT_EQ(user_1_toggled_status, feature()->GetEnabled());
    feature()->SetEnabled(test_case.user_1_expected_status);
    EXPECT_EQ(test_case.user_1_expected_status, feature()->GetEnabled());
  }
}

TEST_F(ScheduledFeatureTest,
       ManualStatusToggleCanPersistAfterResumeFromSuspend) {
  FastForwardTo(MakeTimeOfDay(11, kAM));

  feature()->SetCustomStartTime(MakeTimeOfDay(3, kPM));
  feature()->SetCustomEndTime(MakeTimeOfDay(8, kPM));
  feature()->SetScheduleType(ScheduleType::kCustom);
  EXPECT_FALSE(feature()->GetEnabled());

  // Toggle the status manually and expect that the feature is scheduled to
  // turn back off at 8:00 PM.
  feature()->SetEnabled(true);
  EXPECT_TRUE(feature()->GetEnabled());

  PrefChangeObserver change_log(user1_pref_service(), this);

  // Simulate suspend and then resume at 2:00 PM (which is outside the user's
  // custom schedule). However, the manual toggle to on should be kept.
  AdvanceTimeBy(base::Hours(3));
  feature()->SuspendDone(base::TimeDelta{});
  EXPECT_TRUE(feature()->GetEnabled());

  // Suspend again and resume at 5:00 PM (which is within the user's custom
  // schedule). The schedule should be applied normally.
  AdvanceTimeBy(base::Hours(3));
  feature()->SuspendDone(base::TimeDelta{});
  EXPECT_TRUE(feature()->GetEnabled());

  // Suspend and resume at 9:00 PM and expect the feature to be off.
  AdvanceTimeBy(base::Hours(4));
  feature()->SuspendDone(base::TimeDelta{});
  EXPECT_FALSE(feature()->GetEnabled());

  EXPECT_THAT(change_log.changes(),
              ElementsAre(Pair(MakeTimeOfDay(9, AmPm::kPM), false)));
}

TEST_F(ScheduledFeatureTest, CurrentCheckpointForNoneSchedule) {
  ASSERT_EQ(feature()->current_checkpoint(), ScheduleCheckpoint::kDisabled);

  const CheckpointObserver checkpoint_observer(feature(), this);

  feature()->SetEnabled(true);
  feature()->SetEnabled(true);
  feature()->SetEnabled(false);
  feature()->SetEnabled(false);
  EXPECT_THAT(checkpoint_observer.changes(),
              ElementsAre(Pair(_, ScheduleCheckpoint::kEnabled),
                          Pair(_, ScheduleCheckpoint::kDisabled)));
}

TEST_F(ScheduledFeatureTest, CurrentCheckpointForCustomSchedule) {
  FastForwardTo(MakeTimeOfDay(12, kAM));
  ASSERT_EQ(feature()->current_checkpoint(), ScheduleCheckpoint::kDisabled);

  const CheckpointObserver checkpoint_observer(feature(), this);
  // Checkpoint 0:
  // Custom schedule is enabled from 6 PM to 6 AM, so it should immediately
  // flip to enabled.
  feature()->SetScheduleType(ScheduleType::kCustom);
  // Checkpoint 1:
  // Fast forward to sunrise
  FastForwardTo(MakeTimeOfDay(6, kAM));
  // Checkpoint 2:
  // Fast forward to sunset.
  FastForwardTo(MakeTimeOfDay(6, kPM));
  EXPECT_THAT(
      checkpoint_observer.changes(),
      ElementsAre(Pair(MakeTimeOfDay(12, kAM), ScheduleCheckpoint::kEnabled),
                  Pair(MakeTimeOfDay(6, kAM), ScheduleCheckpoint::kDisabled),
                  Pair(MakeTimeOfDay(6, kPM), ScheduleCheckpoint::kEnabled)));
}

// These reflect real-world combinations of schedule type + feature enabled pref
// changes that can happen with D/L mode.
TEST_F(ScheduledFeatureTest, CurrentCheckpointForSwitchingScheduleTypes) {
  FastForwardTo(MakeTimeOfDay(12, kAM));
  ASSERT_EQ(feature()->current_checkpoint(), ScheduleCheckpoint::kDisabled);

  const CheckpointObserver checkpoint_observer(feature(), this);

  // Checkpoint 0:
  // Sunset is 6 PM and sunrise is 6 AM, so it should immediately flip to
  // enabled.
  feature()->SetScheduleType(ScheduleType::kSunsetToSunrise);

  // Checkpoint 1:
  // Flip back to no schedule type. It should stay enabled.
  feature()->SetScheduleType(ScheduleType::kNone);

  // Checkpoint 2:
  // Fast forward to 10 AM and flip back to sunset to sunrise schedule type.
  // It should automatically flip to disabled, but at the morning checkpoint.
  FastForwardTo(MakeTimeOfDay(10, kAM));
  feature()->SetScheduleType(ScheduleType::kSunsetToSunrise);

  // Checkpoint 2:
  // Flip back to no schedule type. It should stay disabled, but the default
  // checkpoint for disabled is sunrise, not morning, so the checkpoint should
  // change again.
  feature()->SetScheduleType(ScheduleType::kNone);

  // Checkpoint 3:
  // Fast forward to 12 AM again and switch to sunset to sunrise. Feature should
  // automatically flip to enabled.
  FastForwardTo(MakeTimeOfDay(12, kAM));
  feature()->SetScheduleType(ScheduleType::kSunsetToSunrise);

  // Checkpoint 4:
  // Now manually toggle the feature to disabled (opposite of what the schedule
  // says). The current checkpoint should reflect the feature being disabled.
  feature()->SetEnabled(false);

  EXPECT_THAT(
      checkpoint_observer.changes(),
      ElementsAre(Pair(MakeTimeOfDay(12, kAM), ScheduleCheckpoint::kSunset),
                  Pair(MakeTimeOfDay(12, kAM), ScheduleCheckpoint::kEnabled),
                  Pair(MakeTimeOfDay(10, kAM), ScheduleCheckpoint::kMorning),
                  Pair(MakeTimeOfDay(10, kAM), ScheduleCheckpoint::kDisabled),
                  Pair(MakeTimeOfDay(12, kAM), ScheduleCheckpoint::kSunset),
                  Pair(MakeTimeOfDay(12, kAM), ScheduleCheckpoint::kSunrise)));
}

// Tests that the feature gracefully handles failures to get local time:
// b/285187343
TEST_F(ScheduledFeatureTest, HandlesLocalTimeFailuresSunsetToSunrise) {
  // Give test initial start time of 9:00 AM.
  FastForwardTo(MakeTimeOfDay(9, AmPm::kAM));

  const CheckpointObserver checkpoint_observer(feature(), this);
  const PrefChangeObserver pref_change_log(user1_pref_service(), this);

  const FailingLocalTimeConverter failing_local_time_converter;
  SetLocalTimeConverter(&failing_local_time_converter);
  ASSERT_EQ(
      geolocation_controller()->GetSunsetTime(),
      base::unexpected(GeolocationController::SunRiseSetError::kUnavailable));
  ASSERT_EQ(
      geolocation_controller()->GetSunriseTime(),
      base::unexpected(GeolocationController::SunRiseSetError::kUnavailable));

  // Normally, this would retrieve a default sunrise/sunset of 6 AM/PM. But
  // due to local time failure, this should keep the current state (disabled)
  // and started scheduling retries with backoff.
  ASSERT_FALSE(GetEnabled());
  feature()->SetScheduleType(ScheduleType::kSunsetToSunrise);
  EXPECT_FALSE(GetEnabled());

  // Fast forward to 8:00 PM. Normally, sunset is 6:00 PM, so the feature should
  // be enabled now, but since local time is still failing, current state
  // should still be maintained.
  FastForwardTo(MakeTimeOfDay(8, AmPm::kPM));
  EXPECT_FALSE(GetEnabled());

  // Now local time comes back and starts working again.
  SetLocalTimeConverter(nullptr);

  // At the next refresh retry, the schedule should resume normally.
  FastForwardBy(kMaxRefreshBackoff);
  EXPECT_TRUE(GetEnabled());

  // Test a full day of the schedule (24 hours)to make sure schedule resumed
  // normally.
  FastForwardBy(base::Days(1));

  EXPECT_THAT(pref_change_log.changes(),
              // When local time starts working again, we know that it is
              // somewhere between 8:00 PM and 8:00 PM + `kMaxRefreshBackoff`.
              // Due to backoff jitter and other variables, it is difficult to
              // pinpoint the exact timestamp for a test expectation, but that's
              // not critical here. The important thing is that it ultimately
              // updates to the correct state gracefully.
              ElementsAre(Pair(_, true), Pair(MakeTimeOfDay(6, kAM), false),
                          Pair(MakeTimeOfDay(6, kPM), true)));
  EXPECT_THAT(
      checkpoint_observer.changes(),
      // 9 AM: Change to `kSunsetToSunrise` schedule type. Feature is disabled,
      // which defaults to sunrise.
      ElementsAre(
          Pair(MakeTimeOfDay(9, kAM), ScheduleCheckpoint::kSunrise),
          // Timestamp of initial change (when local time starts working
          // again) is not precise. See comment above.
          Pair(_, ScheduleCheckpoint::kSunset),
          Pair(MakeTimeOfDay(6, kAM), ScheduleCheckpoint::kSunrise),
          Pair(MakeTimeOfDay(10, kAM), ScheduleCheckpoint::kMorning),
          Pair(MakeTimeOfDay(4, kPM), ScheduleCheckpoint::kLateAfternoon),
          Pair(MakeTimeOfDay(6, kPM), ScheduleCheckpoint::kSunset)));
}

// Tests that the feature gracefully handles failures to get local time:
// b/285187343
TEST_F(ScheduledFeatureTest, HandlesLocalTimeFailuresCustom) {
  // Start time is at 9:00 AM and end time is at 9:00 PM.
  feature()->SetCustomStartTime(MakeTimeOfDay(9, AmPm::kAM));
  feature()->SetCustomEndTime(MakeTimeOfDay(9, AmPm::kPM));
  // Give test initial start time of 9:00 AM.
  FastForwardTo(MakeTimeOfDay(9, AmPm::kAM));

  const CheckpointObserver checkpoint_observer(feature(), this);
  const PrefChangeObserver pref_change_log(user1_pref_service(), this);

  // Feature should be enabled immediately since it's 9:00 AM.
  feature()->SetScheduleType(ScheduleType::kCustom);
  EXPECT_TRUE(GetEnabled());

  const FailingLocalTimeConverter failing_local_time_converter;
  SetLocalTimeConverter(&failing_local_time_converter);

  // At 9 PM, we switch the feature off as previously scheduled, but local time
  // has started failing. Thus, the feature switches state correctly, but the
  // next refresh (which should come at 9 AM tomorrow) is not scheduled
  // correctly. Retries start.
  FastForwardTo(MakeTimeOfDay(9, AmPm::kPM));
  EXPECT_FALSE(GetEnabled());

  // 9 AM tomorrow. Feature is still disabled due to local time failure.
  FastForwardTo(MakeTimeOfDay(9, AmPm::kAM));
  EXPECT_FALSE(GetEnabled());

  // Local time starts working again. Next refresh should return the schedule to
  // normal.
  SetLocalTimeConverter(nullptr);
  FastForwardBy(kMaxRefreshBackoff);
  EXPECT_TRUE(GetEnabled());

  // Test a full day of the schedule (24 hours) to make sure schedule resumed
  // normally.
  FastForwardBy(base::Days(1));

  EXPECT_THAT(pref_change_log.changes(),
              ElementsAre(Pair(MakeTimeOfDay(9, kAM), true),
                          Pair(MakeTimeOfDay(9, kPM), false),
                          // Local time starts working slightly after 9 AM. See
                          // comments in
                          // `HandlesLocalTimeFailuresSunsetToSunrise` for why
                          // an exact timestamp is not specified.
                          Pair(_, true),
                          // Schedule resumes like normal:
                          Pair(MakeTimeOfDay(9, kPM), false),
                          Pair(MakeTimeOfDay(9, kAM), true)));
  EXPECT_THAT(
      checkpoint_observer.changes(),
      ElementsAre(Pair(MakeTimeOfDay(9, kAM), ScheduleCheckpoint::kEnabled),
                  Pair(MakeTimeOfDay(9, kPM), ScheduleCheckpoint::kDisabled),
                  // Local time starts working slightly after 9 AM. See
                  // comments in
                  // `HandlesLocalTimeFailuresSunsetToSunrise` for why
                  // an exact timestamp is not specified.
                  Pair(_, ScheduleCheckpoint::kEnabled),
                  // Schedule resumes like normal:
                  Pair(MakeTimeOfDay(9, kPM), ScheduleCheckpoint::kDisabled),
                  Pair(MakeTimeOfDay(9, kAM), ScheduleCheckpoint::kEnabled)));
}

TEST_P(ScheduledFeatureGeopositionTest, CyclesThroughCheckpoints) {
  // Sunrise, morning, late afternoon, and sunset
  static constexpr size_t kNumCheckpointsPerDay = 4;

  const CheckpointObserver checkpoint_observer(feature(), this);
  FastForwardBy(base::Days(1));

  // This legitimately happens in regions with no daylight/darkness.
  if (checkpoint_observer.changes().empty()) {
    return;
  }

  const size_t num_checkpoints_observed = checkpoint_observer.changes().size();
  // There are a couple of corner cases where 3 or 5 checkpoints are observed in
  // 24 hours.
  //
  // Example of 5:
  // Now: 5:59 AM
  // Sunrise today: 6:00 AM
  // Sunrise tomorrow: 5:58 AM
  //
  // Expected checkpoint changes:
  // * Sunrise 1 (6 AM)
  // * Morning (10 AM)
  // * Late Afternoon (4 PM)
  // * Sunset (6 PM)
  // * Sunrise 2 (5:58 AM)
  //
  // Example of 3:
  // Now: 6:01 AM
  // Sunrise today: 6:00 AM
  // Sunrise tomorrow: 6:02 AM
  //
  // Expected checkpoint changes:
  // * Morning (10 AM)
  // * Late Afternoon (4 PM)
  // * Sunset (6 PM)
  ASSERT_GE(num_checkpoints_observed, kNumCheckpointsPerDay - 1);
  ASSERT_LE(num_checkpoints_observed, kNumCheckpointsPerDay + 1);
  for (size_t i = 1; i < num_checkpoints_observed; ++i) {
    EXPECT_EQ(
        checkpoint_observer.changes()[i].second,
        GetNextExpectedCheckpoint(checkpoint_observer.changes()[i - 1].second));
  }
}

TEST_F(ScheduledFeatureTest, RecordsScheduleTypeHistogram) {
  const std::string test_histogram_name = "Ash.Test.ScheduleType";
  base::HistogramTester histogram_tester;
  feature()->SetScheduleType(ScheduleType::kCustom);
  histogram_tester.ExpectTotalCount(test_histogram_name, 0);

  feature()->set_schedule_type_histogram_name(test_histogram_name);
  feature()->SetScheduleType(ScheduleType::kSunsetToSunrise);
  feature()->SetScheduleType(ScheduleType::kNone);
  feature()->SetScheduleType(ScheduleType::kCustom);
  histogram_tester.ExpectBucketCount(test_histogram_name, ScheduleType::kNone,
                                     1);
  histogram_tester.ExpectBucketCount(test_histogram_name,
                                     ScheduleType::kSunsetToSunrise, 1);
  histogram_tester.ExpectBucketCount(test_histogram_name, ScheduleType::kCustom,
                                     1);

  // Switching users should not count as a schedule type change even if second
  // user's schedule type is different from the first.
  base::HistogramTester histogram_tester_2;
  const ScheduleType user_1_schedule_type = feature()->GetScheduleType();
  SwitchActiveUser(kUser2Email);
  ASSERT_NE(feature()->GetScheduleType(), user_1_schedule_type);
  histogram_tester_2.ExpectTotalCount(test_histogram_name, 0);
}

}  // namespace

}  // namespace ash