chromium/chrome/browser/ash/power/auto_screen_brightness/modeller_impl_unittest.cc

// 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 "chrome/browser/ash/power/auto_screen_brightness/modeller_impl.h"

#include "ash/constants/ash_features.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/important_file_writer.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_mock_time_task_runner.h"
#include "chrome/browser/ash/power/auto_screen_brightness/fake_brightness_monitor.h"
#include "chrome/browser/ash/power/auto_screen_brightness/fake_light_provider.h"
#include "chrome/browser/ash/power/auto_screen_brightness/fake_model_config_loader.h"
#include "chrome/browser/ash/power/auto_screen_brightness/monotone_cubic_spline.h"
#include "chrome/browser/ash/power/auto_screen_brightness/trainer.h"
#include "chrome/browser/ash/power/auto_screen_brightness/utils.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/user_activity/user_activity_detector.h"
#include "ui/events/event.h"
#include "ui/events/types/event_type.h"

namespace ash {
namespace power {
namespace auto_screen_brightness {

namespace {

MonotoneCubicSpline CreateTestCurveFromTrainingData(
    const std::vector<TrainingDataPoint>& data) {
  CHECK_GT(data.size(), 1u);

  std::vector<double> xs;
  std::vector<double> ys;

  const auto& data_point = data[0];
  xs.push_back(data_point.ambient_log_lux);
  ys.push_back((data_point.brightness_old + data_point.brightness_new) / 2);

  for (size_t i = 1; i < data.size(); ++i) {
    xs.push_back(xs[i - 1] + 1);
    ys.push_back(ys[i - 1] + 1);
  }

  return *MonotoneCubicSpline::CreateMonotoneCubicSpline(xs, ys);
}

void CheckOptionalCurves(
    const std::optional<MonotoneCubicSpline>& result_curve,
    const std::optional<MonotoneCubicSpline>& expected_curve) {
  EXPECT_EQ(result_curve.has_value(), expected_curve.has_value());
  if (result_curve) {
    EXPECT_EQ(*result_curve, *expected_curve);
  }
}

// Fake testing trainer that has configuration status and validity of personal
// curve specified in the constructor.
class FakeTrainer : public Trainer {
 public:
  FakeTrainer(bool is_configured,
              bool is_personal_curve_valid,
              bool return_new_curve,
              double curve_error)
      : is_configured_(is_configured),
        is_personal_curve_valid_(is_personal_curve_valid),
        return_new_curve_(return_new_curve),
        curve_error_(curve_error) {
    // If personal curve is valid, then the trainer must be configured.
    DCHECK(!is_personal_curve_valid_ || is_configured_);
  }

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

  ~FakeTrainer() override = default;

  // Trainer overrides:
  bool HasValidConfiguration() const override { return is_configured_; }

  bool SetInitialCurves(const MonotoneCubicSpline& global_curve,
                        const MonotoneCubicSpline& current_curve) override {
    DCHECK(is_configured_);
    global_curve_ = global_curve;
    current_curve_ = is_personal_curve_valid_ ? current_curve : global_curve;
    return is_personal_curve_valid_;
  }

  MonotoneCubicSpline GetGlobalCurve() const override {
    DCHECK(is_configured_);
    DCHECK(global_curve_);
    return *global_curve_;
  }

  MonotoneCubicSpline GetCurrentCurve() const override {
    DCHECK(is_configured_);
    DCHECK(current_curve_);
    return *current_curve_;
  }

  TrainingResult Train(const std::vector<TrainingDataPoint>& data) override {
    if (!return_new_curve_) {
      return TrainingResult(std::nullopt, curve_error_);
    }

    DCHECK(is_configured_);
    DCHECK(current_curve_);
    std::vector<TrainingDataPoint> used_data = data;

    // We need at least 2 points to create a MonotoneCubicSpline. Hence we
    // insert another one if |data| has only 1 point.
    if (data.size() == 1) {
      used_data.push_back(data[0]);
    }
    current_curve_ = CreateTestCurveFromTrainingData(used_data);
    return TrainingResult(current_curve_, curve_error_);
  }

 private:
  bool is_configured_;
  bool is_personal_curve_valid_;
  std::optional<MonotoneCubicSpline> global_curve_;
  std::optional<MonotoneCubicSpline> current_curve_;

  bool return_new_curve_ = false;
  double curve_error_ = 0.0;
};

class TestObserver : public Modeller::Observer {
 public:
  TestObserver() {}

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

  ~TestObserver() override = default;

  // Modeller::Observer overrides:
  void OnModelTrained(const MonotoneCubicSpline& brightness_curve) override {
    model_.personal_curve = brightness_curve;
    ++model_.iteration_count;
  }

  void OnModelInitialized(const Model& model) override {
    model_initialized_ = true;
    model_ = model;
  }

  std::optional<MonotoneCubicSpline> personal_curve() const {
    return model_.personal_curve;
  }

  int iteration_count() const { return model_.iteration_count; }

  void CheckStatus(bool is_model_initialized, const Model& expected_model) {
    EXPECT_EQ(is_model_initialized, model_initialized_);
    CheckOptionalCurves(expected_model.global_curve, model_.global_curve);
    CheckOptionalCurves(expected_model.personal_curve, model_.personal_curve);
    EXPECT_EQ(expected_model.iteration_count, model_.iteration_count);
  }

 private:
  bool model_initialized_ = false;
  Model model_;
};

}  // namespace

class ModellerImplTest : public testing::Test {
 public:
  ModellerImplTest()
      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
    CHECK(temp_dir_.CreateUniqueTempDir());
    TestingProfile::Builder profile_builder;
    profile_builder.SetProfileName("[email protected]");
    profile_builder.SetPath(temp_dir_.GetPath().AppendASCII("TestProfile"));
    profile_ = profile_builder.Build();
    test_model_config_ = GetTestModelConfig();
    test_initial_global_curve_ = MonotoneCubicSpline::CreateMonotoneCubicSpline(
        test_model_config_.log_lux, test_model_config_.brightness);
    DCHECK(test_initial_global_curve_);

    als_reader_ = std::make_unique<AlsReader>();
    fake_light_provider_ =
        std::make_unique<FakeLightProvider>(als_reader_.get());
  }

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

  ~ModellerImplTest() override {
    base::ThreadPoolInstance::Get()->FlushForTesting();
  }

  // Sets up |modeller_| with a FakeTrainer.
  void SetUpModeller(bool is_trainer_configured,
                     bool is_personal_curve_valid,
                     bool return_new_curve,
                     double curve_error) {
    modeller_ = ModellerImpl::CreateForTesting(
        profile_.get(), als_reader_.get(), &fake_brightness_monitor_,
        &fake_model_config_loader_, ui::UserActivityDetector::Get(),
        std::make_unique<FakeTrainer>(is_trainer_configured,
                                      is_personal_curve_valid, return_new_curve,
                                      curve_error),
        base::SequencedTaskRunner::GetCurrentDefault(),
        task_environment_.GetMockTickClock());

    test_observer_ = std::make_unique<TestObserver>();
    modeller_->AddObserver(test_observer_.get());
  }

  void Init(AlsReader::AlsInitStatus als_reader_status,
            BrightnessMonitor::Status brightness_monitor_status,
            std::optional<ModelConfig> model_config,
            bool is_trainer_configured = true,
            bool is_personal_curve_valid = true,
            bool return_new_curve = true,
            double curve_error = 0.0,
            const std::map<std::string, std::string>& params = {}) {
    if (!params.empty()) {
      scoped_feature_list_.InitAndEnableFeatureWithParameters(
          features::kAutoScreenBrightness, params);
    }

    fake_light_provider_->set_als_init_status(als_reader_status);
    fake_brightness_monitor_.set_status(brightness_monitor_status);
    if (model_config) {
      fake_model_config_loader_.set_model_config(model_config.value());
    }

    SetUpModeller(is_trainer_configured, is_personal_curve_valid,
                  return_new_curve, curve_error);
    task_environment_.RunUntilIdle();
  }

 protected:
  void WriteModelToFile(const Model& model) {
    const ModellerImpl::ModelSavingSpec& model_saving_spec =
        ModellerImpl::ModellerImpl::GetModelSavingSpecFromProfilePath(
            profile_->GetPath());
    CHECK(!model_saving_spec.global_curve.empty());
    CHECK(!model_saving_spec.personal_curve.empty());
    CHECK(!model_saving_spec.iteration_count.empty());
    SaveModelToDisk(model_saving_spec, model, true /* save_global_curve */,
                    true /* save_personal_curve */, true /* is_testing */);
  }

  // Returns a valid ModelConfig.
  ModelConfig GetTestModelConfig() {
    ModelConfig model_config;
    model_config.auto_brightness_als_horizon_seconds = 2.0;
    model_config.log_lux = {
        3.69, 4.83, 6.54, 7.68, 8.25, 8.82,
    };
    model_config.brightness = {
        36.14, 47.62, 85.83, 93.27, 93.27, 100,
    };

    model_config.metrics_key = "abc";
    model_config.model_als_horizon_seconds = 3.0;
    return model_config;
  }

  content::BrowserTaskEnvironment task_environment_;
  base::HistogramTester histogram_tester_;

  base::ScopedTempDir temp_dir_;
  std::unique_ptr<TestingProfile> profile_;

  ModelConfig test_model_config_;
  std::optional<MonotoneCubicSpline> test_initial_global_curve_;

  std::unique_ptr<FakeLightProvider> fake_light_provider_;
  std::unique_ptr<AlsReader> als_reader_;
  FakeBrightnessMonitor fake_brightness_monitor_;
  FakeModelConfigLoader fake_model_config_loader_;

  std::unique_ptr<ModellerImpl> modeller_;
  std::unique_ptr<TestObserver> test_observer_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

// AlsReader is |kDisabled| when Modeller is created.
TEST_F(ModellerImplTest, AlsReaderDisabledOnInit) {
  Init(AlsReader::AlsInitStatus::kDisabled, BrightnessMonitor::Status::kSuccess,
       test_model_config_);

  // Model should be empty if modeller is disabled.
  test_observer_->CheckStatus(true /* is_model_initialized */, Model());
}

// BrightnessMonitor is |kDisabled| when Modeller is created.
TEST_F(ModellerImplTest, BrightnessMonitorDisabledOnInit) {
  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kDisabled,
       test_model_config_);

  // Model should be empty if modeller is disabled.
  test_observer_->CheckStatus(true /* is_model_initialized */, Model());
}

// ModelConfigLoader has an invalid config, hence Modeller is disabled.
TEST_F(ModellerImplTest, ModelConfigLoaderDisabledOnInit) {
  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       ModelConfig());

  // Model should be empty if modeller is disabled.
  test_observer_->CheckStatus(true /* is_model_initialized */, Model());
}

// AlsReader is |kDisabled| on later notification.
TEST_F(ModellerImplTest, AlsReaderDisabledOnNotification) {
  Init(AlsReader::AlsInitStatus::kInProgress,
       BrightnessMonitor::Status::kSuccess, test_model_config_);

  test_observer_->CheckStatus(false /* is_model_initialized */, Model());

  fake_light_provider_->set_als_init_status(
      AlsReader::AlsInitStatus::kDisabled);
  fake_light_provider_->ReportReaderInitialized();
  task_environment_.RunUntilIdle();

  // Model should be empty if modeller is disabled.
  test_observer_->CheckStatus(true /* is_model_initialized */, Model());
}

// AlsReader is |kSuccess| on later notification.
TEST_F(ModellerImplTest, AlsReaderEnabledOnNotification) {
  Init(AlsReader::AlsInitStatus::kInProgress,
       BrightnessMonitor::Status::kSuccess, test_model_config_);

  test_observer_->CheckStatus(false /* is_model_initialized */, Model());

  fake_light_provider_->set_als_init_status(AlsReader::AlsInitStatus::kSuccess);
  fake_light_provider_->ReportReaderInitialized();
  task_environment_.RunUntilIdle();

  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);
}

// BrightnessMonitor is |kDisabled| on later notification.
TEST_F(ModellerImplTest, BrightnessMonitorDisabledOnNotification) {
  Init(AlsReader::AlsInitStatus::kSuccess,
       BrightnessMonitor::Status::kInitializing, test_model_config_);

  test_observer_->CheckStatus(false /* is_model_initialized */, Model());

  fake_brightness_monitor_.set_status(BrightnessMonitor::Status::kDisabled);
  fake_brightness_monitor_.ReportBrightnessMonitorInitialized();

  // Model should be empty if modeller is disabled.
  test_observer_->CheckStatus(true /* is_model_initialized */, Model());
}

// BrightnessMonitor is |kSuccess| on later notification.
TEST_F(ModellerImplTest, BrightnessMonitorEnabledOnNotification) {
  Init(AlsReader::AlsInitStatus::kSuccess,
       BrightnessMonitor::Status::kInitializing, test_model_config_);

  test_observer_->CheckStatus(false /* is_model_initialized */, Model());

  fake_brightness_monitor_.set_status(BrightnessMonitor::Status::kSuccess);
  fake_brightness_monitor_.ReportBrightnessMonitorInitialized();
  task_environment_.RunUntilIdle();

  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);
}

// ModelConfigLoader reports an invalid config on later notification.
TEST_F(ModellerImplTest, InvalidModelConfigOnNotification) {
  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       std::nullopt /* model_config */);

  test_observer_->CheckStatus(false /* is_model_initialized */, Model());

  // ModelConfig() creates an invalid config.
  DCHECK(!IsValidModelConfig(ModelConfig()));
  fake_model_config_loader_.set_model_config(ModelConfig());
  fake_model_config_loader_.ReportModelConfigLoaded();
  task_environment_.RunUntilIdle();
  test_observer_->CheckStatus(true /* is_model_initialized */, Model());
}

// ModelConfigLoader reports a valid config on later notification.
TEST_F(ModellerImplTest, ValidModelConfigOnNotification) {
  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       std::nullopt /* model_config */);

  test_observer_->CheckStatus(false /* is_model_initialized */, Model());

  fake_model_config_loader_.set_model_config(test_model_config_);
  fake_model_config_loader_.ReportModelConfigLoaded();
  task_environment_.RunUntilIdle();

  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);
}

// A model is loaded from disk, this is a personal curve, and the saved global
// curve is the same as initial global curve set from model config, hence there
// is no need to reset the model.
TEST_F(ModellerImplTest, ModelLoadedFromProfilePath) {
  const std::vector<double> xs = {0, 10, 20, 40, 60, 80, 90, 100};
  const std::vector<double> ys = {0, 5, 10, 15, 20, 25, 30, 40};
  const std::optional<MonotoneCubicSpline> personal_curve =
      MonotoneCubicSpline::CreateMonotoneCubicSpline(xs, ys);
  DCHECK(personal_curve);

  // Use |test_initial_global_curve_| as the saved global curve.
  const Model model(test_initial_global_curve_, personal_curve,
                    1 /* iteration_count */);
  WriteModelToFile(model);

  task_environment_.RunUntilIdle();

  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       test_model_config_);
  task_environment_.RunUntilIdle();

  test_observer_->CheckStatus(true /* is_model_initialized */, model);
  histogram_tester_.ExpectUniqueSample(
      "AutoScreenBrightness.PersonalCurveValid", true, 1);
}

// A model is loaded from disk, this is a personal curve, and the saved global
// curve is different from the initial global curve set from model config, hence
// there the model is reset.
TEST_F(ModellerImplTest, ModelLoadedFromProfilePathWithReset) {
  const std::vector<double> xs = {0, 10, 20, 40, 60, 80, 90, 100};
  const std::vector<double> ys = {0, 5, 10, 15, 20, 25, 30, 40};
  const std::optional<MonotoneCubicSpline> saved_global_curve =
      MonotoneCubicSpline::CreateMonotoneCubicSpline(xs, ys);
  DCHECK(saved_global_curve);

  const Model model(saved_global_curve, saved_global_curve,
                    2 /* iteration_count */);
  WriteModelToFile(model);

  task_environment_.RunUntilIdle();

  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       test_model_config_);

  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);

  histogram_tester_.ExpectUniqueSample(
      "AutoScreenBrightness.PersonalCurveValid", true, 1);
}

// A model is loaded from disk but the personal curve doesn't satisfy Trainer
// slope constraint, hence it's ignored and the global curve is used instead.
TEST_F(ModellerImplTest, PersonalCurveError) {
  const Model model(test_initial_global_curve_, test_initial_global_curve_, 2);
  WriteModelToFile(model);
  task_environment_.RunUntilIdle();

  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       test_model_config_, true /* is_trainer_configured */,
       false /* is_personal_curve_valid */);

  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);

  histogram_tester_.ExpectUniqueSample(
      "AutoScreenBrightness.PersonalCurveValid", false, 1);
}

// Ambient light values are received. We check average ambient light has been
// calculated from the recent samples only.
TEST_F(ModellerImplTest, OnAmbientLightUpdated) {
  const int horizon_in_seconds = test_model_config_.model_als_horizon_seconds;

  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       test_model_config_, true /* is_trainer_configured */,
       true /* is_personal_curve_valid */);

  // No model is saved to disk, hence the initial model only has the global
  // curve set from the config.
  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);

  EXPECT_EQ(modeller_->GetModelConfigForTesting(), test_model_config_);

  const int first_lux = 1000;
  double running_sum = 0.0;
  for (int i = 0; i < horizon_in_seconds; ++i) {
    task_environment_.FastForwardBy(base::Seconds(1));
    const int lux = i == 0 ? first_lux : i;
    fake_light_provider_->ReportAmbientLightUpdate(lux);
    running_sum += ConvertToLog(lux);
    EXPECT_DOUBLE_EQ(
        modeller_->AverageAmbientForTesting(task_environment_.NowTicks())
            .value(),
        running_sum / (i + 1));
  }
  EXPECT_EQ(test_observer_->iteration_count(), 0);

  // Add another one should push the oldest |first_lux| out of the horizon.
  task_environment_.FastForwardBy(base::Seconds(1));
  fake_light_provider_->ReportAmbientLightUpdate(100);
  running_sum = running_sum + ConvertToLog(100) - ConvertToLog(first_lux);
  EXPECT_DOUBLE_EQ(
      modeller_->AverageAmbientForTesting(task_environment_.NowTicks()).value(),
      running_sum / horizon_in_seconds);
  EXPECT_EQ(test_observer_->iteration_count(), 0);
}

// User brightness changes are received, training example cache reaches
// |max_training_data_points_| to trigger early training. This all happens
// within a small window shorter than |training_delay_|.
TEST_F(ModellerImplTest, OnUserBrightnessChanged) {
  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       test_model_config_, true /* is_trainer_configured */,
       true /* is_personal_curve_valid */, true /* return_new_curve */,
       0.0 /* curve_error */,
       {{"training_delay_in_seconds", base::NumberToString(60)}});

  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);

  std::vector<TrainingDataPoint> expected_data;

  for (size_t i = 0; i < modeller_->GetMaxTrainingDataPointsForTesting() - 1;
       ++i) {
    EXPECT_EQ(i, modeller_->NumberTrainingDataPointsForTesting());
    task_environment_.FastForwardBy(base::Milliseconds(1));
    const base::TimeTicks now = task_environment_.NowTicks();
    const int lux = i * 20;
    fake_light_provider_->ReportAmbientLightUpdate(lux);
    const double brightness_old = 10.0 + i;
    const double brightness_new = 20.0 + i;
    modeller_->OnUserBrightnessChanged(brightness_old, brightness_new);
    expected_data.push_back({brightness_old, brightness_new,
                             modeller_->AverageAmbientForTesting(now).value(),
                             now});
    EXPECT_EQ(test_observer_->iteration_count(), 0);
  }

  // Training should not have started.
  EXPECT_EQ(modeller_->GetMaxTrainingDataPointsForTesting() - 1,
            modeller_->NumberTrainingDataPointsForTesting());

  // Add one more data point to trigger the training early.
  task_environment_.FastForwardBy(base::Milliseconds(1));
  const base::TimeTicks now = task_environment_.NowTicks();
  const double brightness_old = 85;
  const double brightness_new = 95;
  modeller_->OnUserBrightnessChanged(brightness_old, brightness_new);
  expected_data.push_back({brightness_old, brightness_new,
                           modeller_->AverageAmbientForTesting(now).value(),
                           now});
  task_environment_.RunUntilIdle();

  EXPECT_EQ(0u, modeller_->NumberTrainingDataPointsForTesting());
  EXPECT_EQ(test_observer_->iteration_count(), 1);

  const std::optional<MonotoneCubicSpline>& result_curve =
      test_observer_->personal_curve();
  DCHECK(result_curve);

  const MonotoneCubicSpline expected_curve =
      CreateTestCurveFromTrainingData(expected_data);
  EXPECT_EQ(expected_curve, *result_curve);
}

// User activities resets timer used to start training.
TEST_F(ModellerImplTest, MultipleUserActivities) {
  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       test_model_config_, true /* is_trainer_configured */,
       true /* is_personal_curve_valid */, true /* return_new_curve */,
       0.0 /* curve_error */,
       {{"training_delay_in_seconds", base::NumberToString(60)}});

  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);

  task_environment_.FastForwardBy(base::Seconds(1));
  fake_light_provider_->ReportAmbientLightUpdate(30);
  std::vector<TrainingDataPoint> expected_data;
  for (size_t i = 0; i < 10; ++i) {
    EXPECT_EQ(i, modeller_->NumberTrainingDataPointsForTesting());
    task_environment_.FastForwardBy(base::Milliseconds(1));
    const base::TimeTicks now = task_environment_.NowTicks();
    const int lux = i * 20;
    fake_light_provider_->ReportAmbientLightUpdate(lux);
    const double brightness_old = 10.0 + i;
    const double brightness_new = 20.0 + i;
    modeller_->OnUserBrightnessChanged(brightness_old, brightness_new);
    expected_data.push_back({brightness_old, brightness_new,
                             modeller_->AverageAmbientForTesting(now).value(),
                             now});
    EXPECT_EQ(test_observer_->iteration_count(), 0);
  }

  EXPECT_EQ(modeller_->NumberTrainingDataPointsForTesting(), 10u);

  task_environment_.FastForwardBy(modeller_->GetTrainingDelayForTesting() / 2);
  // A user activity is received, timer should be reset.
  const ui::MouseEvent mouse_event(ui::EventType::kMouseExited,
                                   gfx::Point(0, 0), gfx::Point(0, 0),
                                   base::TimeTicks(), 0, 0);
  modeller_->OnUserActivity(&mouse_event);

  task_environment_.FastForwardBy(modeller_->GetTrainingDelayForTesting() / 3);
  EXPECT_EQ(modeller_->NumberTrainingDataPointsForTesting(), 10u);
  EXPECT_EQ(test_observer_->iteration_count(), 0);

  // Another user event is received.
  modeller_->OnUserActivity(&mouse_event);

  // After |training_delay_|/2, no training has started.
  task_environment_.FastForwardBy(modeller_->GetTrainingDelayForTesting() / 2);
  EXPECT_EQ(modeller_->NumberTrainingDataPointsForTesting(), 10u);
  EXPECT_EQ(test_observer_->iteration_count(), 0);

  // After another |training_delay_|/2, training is scheduled.
  task_environment_.FastForwardBy(modeller_->GetTrainingDelayForTesting() / 2);

  EXPECT_EQ(0u, modeller_->NumberTrainingDataPointsForTesting());
  EXPECT_EQ(test_observer_->iteration_count(), 1);
  const std::optional<MonotoneCubicSpline>& result_curve =
      test_observer_->personal_curve();
  DCHECK(result_curve);

  const MonotoneCubicSpline expected_curve =
      CreateTestCurveFromTrainingData(expected_data);
  EXPECT_EQ(expected_curve, *result_curve);
}

// Training delay is 0, hence we train as soon as we have 1 data point.
TEST_F(ModellerImplTest, ZeroTrainingDelay) {
  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       test_model_config_, true /* is_trainer_configured */,
       true /* is_personal_curve_valid  */, true /* return_new_curve */,
       0.0 /* curve_error */,
       {
           {"training_delay_in_seconds", "0"},
       });

  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);

  fake_light_provider_->ReportAmbientLightUpdate(30);
  const ui::MouseEvent mouse_event(ui::EventType::kMouseExited,
                                   gfx::Point(0, 0), gfx::Point(0, 0),
                                   base::TimeTicks(), 0, 0);
  modeller_->OnUserActivity(&mouse_event);

  modeller_->OnUserBrightnessChanged(10, 20);
  task_environment_.RunUntilIdle();
  EXPECT_EQ(0u, modeller_->NumberTrainingDataPointsForTesting());
  EXPECT_EQ(test_observer_->iteration_count(), 1);
}

// Curve is not updated and so model isn't exported.
TEST_F(ModellerImplTest, CurveUnchanged) {
  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       test_model_config_, true /* is_trainer_configured */,
       true /* is_personal_curve_valid  */, false /* return_new_curve */,
       0.0 /* curve_error */,
       {
           {"training_delay_in_seconds", "0"},
       });

  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);

  fake_light_provider_->ReportAmbientLightUpdate(30);

  modeller_->OnUserBrightnessChanged(10, 20);
  task_environment_.RunUntilIdle();
  EXPECT_EQ(0u, modeller_->NumberTrainingDataPointsForTesting());
  EXPECT_EQ(test_observer_->iteration_count(), 0);
}

// Curve is updated but error is above the threshold, hence model isn't
// exported.
TEST_F(ModellerImplTest, CurveChangedLargeError) {
  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       test_model_config_, true /* is_trainer_configured */,
       true /* is_personal_curve_valid  */, true /* return_new_curve */,
       10.0 /* curve_error */,
       {
           {"training_delay_in_seconds", "0"},
           {"curve_error_tolerance", "5"},
       });

  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);

  fake_light_provider_->ReportAmbientLightUpdate(30);

  modeller_->OnUserBrightnessChanged(10, 20);
  task_environment_.RunUntilIdle();
  EXPECT_EQ(0u, modeller_->NumberTrainingDataPointsForTesting());
  EXPECT_EQ(test_observer_->iteration_count(), 0);
}

// Curve is updated and error is not above the threshold, hence model is
// exported.
TEST_F(ModellerImplTest, CurveChangedSmallError) {
  Init(AlsReader::AlsInitStatus::kSuccess, BrightnessMonitor::Status::kSuccess,
       test_model_config_, true /* is_trainer_configured */,
       true /* is_personal_curve_valid  */, true /* return_new_curve */,
       10.0 /* curve_error */,
       {
           {"training_delay_in_seconds", "0"},
           {"curve_error_tolerance", "10"},
       });

  const Model expected_model(test_initial_global_curve_, std::nullopt, 0);
  test_observer_->CheckStatus(true /* is_model_initialized */, expected_model);

  fake_light_provider_->ReportAmbientLightUpdate(30);
  modeller_->OnUserBrightnessChanged(10, 20);
  task_environment_.RunUntilIdle();

  EXPECT_EQ(0u, modeller_->NumberTrainingDataPointsForTesting());
  EXPECT_EQ(test_observer_->iteration_count(), 1);
}

}  // namespace auto_screen_brightness
}  // namespace power
}  // namespace ash