// Copyright 2023 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/ui/safety_hub/safety_hub_service.h"

#include <memory>
#include <string>
#include <utility>

#include "base/run_loop.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/ui/safety_hub/safety_hub_result.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {
constexpr base::TimeDelta kUpdateIntervalForTest = base::Days(7);

class MockSafetyHubResult : public SafetyHubResult {
 public:
  explicit MockSafetyHubResult(base::Time timestamp = base::Time::Now())
      : SafetyHubResult(timestamp) {}
  ~MockSafetyHubResult() override = default;

  std::unique_ptr<SafetyHubResult> Clone() const override {
    return std::make_unique<MockSafetyHubResult>(*this);
  }

  base::DictValue ToDictValue() const override { return BaseToDictValue(); }

  bool IsTriggerForMenuNotification() const override { return true; }

  bool WarrantsNewMenuNotification(
      const base::DictValue& previousResult) const override {
    return true;
  }

  std::u16string GetNotificationString() const override {
    return std::u16string();
  }

  int GetNotificationCommandId() const override { return 0; }

  int GetVal() const { return val_; }

  void SetVal(int val) { val_ = val; }

  void IncreaseVal() { ++val_; }

 private:
  int val_ = 0;
};

class MockSafetyHubService : public SafetyHubService {
 public:
  MockSafetyHubService() {
    // Note: for testing purposes, the repeated updates are not started in the
    // constructor.

    InitializeLatestResult();
  }

  // Returns the number of times that the UpdateOnBackgroundThread function was
  // called.
  int GetNumBackgroundUpdates() const { return num_updates_background_; }

  // Returns the number of times that the UpdateOnBackgroundThread function was
  // called.
  int GetNumUIUpdates() const { return num_updates_ui_; }

  // Set the latest result to a specific value - 42 in this case.
  std::unique_ptr<SafetyHubResult> InitializeLatestResultImpl() override {
    auto init_result = std::make_unique<MockSafetyHubResult>();
    init_result->SetVal(42);
    return init_result;
  }

 protected:
  // For testing purposes, the UpdateOnBackgroundThread function will be
  // executed every seven days.
  base::TimeDelta GetRepeatedUpdateInterval() override {
    return kUpdateIntervalForTest;
  }

  base::OnceCallback<std::unique_ptr<SafetyHubResult>()> GetBackgroundTask()
      override {
    auto init_result = std::make_unique<MockSafetyHubResult>();
    return base::BindOnce(&UpdateOnBackgroundThread, std::move(init_result));
  }

  static std::unique_ptr<SafetyHubResult> UpdateOnBackgroundThread(
      std::unique_ptr<SafetyHubResult> result) {
    auto background_result = std::make_unique<MockSafetyHubResult>();
    background_result->IncreaseVal();
    return background_result;
  }

  std::unique_ptr<SafetyHubResult> UpdateOnUIThread(
      std::unique_ptr<SafetyHubResult> result) override {
    num_updates_background_ +=
        static_cast<MockSafetyHubResult*>(result.get())->GetVal();
    ++num_updates_ui_;
    return std::make_unique<MockSafetyHubResult>();
  }

  base::WeakPtr<SafetyHubService> GetAsWeakRef() override {
    return weak_factory_.GetWeakPtr();
  }

 private:
  int num_updates_background_ = 0;
  int num_updates_ui_ = 0;

  base::WeakPtrFactory<MockSafetyHubService> weak_factory_{this};
};

class MockObserver : public SafetyHubService::Observer {
 public:
  void OnResultAvailable(const SafetyHubResult* result) override {
    ++num_calls_;
    if (callback_) {
      callback_.Run();
    }
  }

  void SetCallback(const base::RepeatingClosure& callback) {
    callback_ = callback;
  }

  // Returns the number of times that the observer was notified.
  int GetNumCalls() const { return num_calls_; }

 private:
  int num_calls_ = 0;
  base::RepeatingClosure callback_;
};

}  // namespace

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

  void SetUp() override { service_ = std::make_unique<MockSafetyHubService>(); }

  void TearDown() override { service_->Shutdown(); }

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

  MockSafetyHubService* service() { return service_.get(); }

 private:
  std::unique_ptr<MockSafetyHubService> service_;

  content::BrowserTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
};

TEST_F(SafetyHubServiceTest, ManageObservers) {
  auto observer = std::make_unique<MockObserver>();
  // The list of observers should initially be empty.
  EXPECT_FALSE(service()->observers_.HasObserver(observer.get()));
  service()->AddObserver(observer.get());
  // After adding the observer, the observer should be registered in the list of
  // observers, but should not be called yet.
  EXPECT_TRUE(service()->observers_.HasObserver(observer.get()));
  EXPECT_EQ(observer->GetNumCalls(), 0);
  auto result = std::make_unique<MockSafetyHubResult>();
  // Notify all observers.
  service()->NotifyObservers(result.get());
  // Ensure that the observer was called just once.
  EXPECT_EQ(observer->GetNumCalls(), 1);
  service()->NotifyObservers(result.get());
  // Ensure that the observer was called again.
  EXPECT_EQ(observer->GetNumCalls(), 2);
  service()->RemoveObserver(observer.get());
  // The observer should be removed from the list of observers, and not get
  // called again when observers are notified.
  EXPECT_FALSE(service()->observers_.HasObserver(observer.get()));
  service()->NotifyObservers(result.get());
  EXPECT_EQ(observer->GetNumCalls(), 2);
}

TEST_F(SafetyHubServiceTest, UpdateOnBackgroundThread) {
  EXPECT_EQ(service()->GetNumUIUpdates(), 0);
  EXPECT_EQ(service()->GetNumBackgroundUpdates(), 0);
  // As long as StartRepeatedUpdates() has not been called, no updates should
  // be made.
  FastForwardBy(kUpdateIntervalForTest);
  EXPECT_EQ(service()->GetNumUIUpdates(), 0);
  EXPECT_EQ(service()->GetNumBackgroundUpdates(), 0);
  // The update will be run asynchronously as soon as StartRepeatedUpdates() is
  // called.
  base::RunLoop loop;
  auto observer = std::make_shared<MockObserver>();
  observer->SetCallback(loop.QuitClosure());
  service()->AddObserver(observer.get());
  service()->StartRepeatedUpdates();
  loop.Run();
  EXPECT_EQ(service()->GetNumUIUpdates(), 1);
  EXPECT_EQ(service()->GetNumBackgroundUpdates(), 1);
  // Move forward a full update interval, which will trigger another update.
  base::RunLoop loop2;
  observer->SetCallback(loop2.QuitClosure());
  FastForwardBy(kUpdateIntervalForTest);
  loop2.Run();
  EXPECT_EQ(service()->GetNumUIUpdates(), 2);
  EXPECT_EQ(service()->GetNumBackgroundUpdates(), 2);
}

TEST_F(SafetyHubServiceTest, GetCachedResult) {
  // The mock service initializes the latest result on construction, so its
  // value should be those that we'd expect.
  std::optional<std::unique_ptr<SafetyHubResult>> opt_result =
      service()->GetCachedResult();
  EXPECT_TRUE(opt_result.has_value());
  MockSafetyHubResult* result =
      static_cast<MockSafetyHubResult*>(opt_result.value().get());
  EXPECT_EQ(result->GetVal(), 42);
}
