// 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 "chrome/browser/dbus_memory_pressure_evaluator_linux.h"

#include <algorithm>
#include <memory>

#include "base/functional/bind.h"
#include "base/memory/memory_pressure_listener.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/notreached.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "dbus/mock_bus.h"
#include "dbus/mock_object_proxy.h"
#include "testing/gtest/include/gtest/gtest.h"

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

namespace {

class MockMemoryPressureVoter : public memory_pressure::MemoryPressureVoter {
 public:
  MOCK_METHOD2(SetVote, void(base::MemoryPressureLevel, bool));
};

}  // namespace

class DbusMemoryPressureEvaluatorLinuxTest : public testing::Test {
 protected:
  static const char* const kLmmService;
  static const char* const kLmmInterface;
  static const char* const kXdgPortalService;
  static const char* const kXdgPortalMemoryMonitorInterface;

  const base::TimeDelta kResetVotePeriod =
      DbusMemoryPressureEvaluatorLinux::kResetVotePeriod;

  void SetUp() override {
    mock_bus_ = base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());

    ON_CALL(*mock_bus_, GetDBusTaskRunner)
        .WillByDefault(
            Return(task_environment_.GetMainThreadTaskRunner().get()));

    SetupProxy(dbus_proxy_, DBUS_SERVICE_DBUS, DBUS_PATH_DBUS);
    SetupProxy(lmm_proxy_, kLmmService,
               DbusMemoryPressureEvaluatorLinux::kLmmObject);
    SetupProxy(portal_proxy_, kXdgPortalService,
               DbusMemoryPressureEvaluatorLinux::kXdgPortalObject);

    ON_CALL(*dbus_proxy_, CallMethod)
        .WillByDefault(Invoke(
            this, &DbusMemoryPressureEvaluatorLinuxTest::HandleDBusCalls));

    auto voter = std::make_unique<MockMemoryPressureVoter>();
    mock_voter_ = voter.get();
    evaluator_ = base::WrapUnique(new DbusMemoryPressureEvaluatorLinux(
        std::move(voter), mock_bus_, mock_bus_));
  }

  void EmitLowMemoryWarning(uint8_t level) {
    evaluator_->OnLowMemoryWarning(base::ok(std::make_tuple(level)));
  }

  // Any service name added via this call will be reported as running by any
  // HasNameOwner calls the evaluator makes.
  void AddRunningService(std::string service) {
    running_services_.push_back(std::move(service));
  }

  void RunServiceChecks() { evaluator()->CheckIfLmmIsAvailable(); }

  base::test::SingleThreadTaskEnvironment& task_environment() {
    return task_environment_;
  }

  dbus::MockObjectProxy* lmm_proxy() { return lmm_proxy_.get(); }
  dbus::MockObjectProxy* portal_proxy() { return portal_proxy_.get(); }

  MockMemoryPressureVoter* mock_voter() { return mock_voter_; }
  DbusMemoryPressureEvaluatorLinux* evaluator() { return evaluator_.get(); }

 private:
  void SetupProxy(scoped_refptr<dbus::MockObjectProxy>& proxy,
                  const char* service,
                  const char* object_path_value) {
    dbus::ObjectPath object_path(object_path_value);
    proxy = base::MakeRefCounted<dbus::MockObjectProxy>(mock_bus_.get(),
                                                        service, object_path);
    ON_CALL(*mock_bus_, GetObjectProxy(service, object_path))
        .WillByDefault(Return(proxy.get()));
  }

  void HandleDBusCalls(dbus::MethodCall* method_call,
                       int timeout_ms,
                       dbus::ObjectProxy::ResponseCallback response_callback) {
    method_call->SetSerial(123);
    method_call->SetReplySerial(456);

    if (method_call->GetMember() == "NameHasOwner") {
      dbus::MessageReader reader(method_call);
      std::string service;
      CHECK(reader.PopString(&service));

      std::unique_ptr<dbus::Response> response =
          dbus::Response::FromMethodCall(method_call);
      dbus::MessageWriter writer(response.get());
      writer.AppendBool(std::ranges::contains(running_services_, service));

      std::move(response_callback).Run(response.get());
    } else if (method_call->GetMember() == "ListActivatableNames") {
      std::unique_ptr<dbus::Response> response =
          dbus::Response::FromMethodCall(method_call);
      dbus::MessageWriter writer(response.get());
      writer.AppendArrayOfStrings({});

      std::move(response_callback).Run(response.get());
    } else {
      NOTREACHED() << method_call->GetMember();
    }
  }

  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::TaskEnvironment::MainThreadType::IO,
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};

  scoped_refptr<dbus::MockBus> mock_bus_;
  scoped_refptr<dbus::MockObjectProxy> dbus_proxy_;
  scoped_refptr<dbus::MockObjectProxy> lmm_proxy_;
  scoped_refptr<dbus::MockObjectProxy> portal_proxy_;

  std::unique_ptr<DbusMemoryPressureEvaluatorLinux> evaluator_;
  raw_ptr<MockMemoryPressureVoter> mock_voter_ = nullptr;

  std::vector<std::string> running_services_;
};

const char* const DbusMemoryPressureEvaluatorLinuxTest::kLmmService =
    DbusMemoryPressureEvaluatorLinux::kLmmService;
const char* const DbusMemoryPressureEvaluatorLinuxTest::kLmmInterface =
    DbusMemoryPressureEvaluatorLinux::kLmmInterface;
const char* const DbusMemoryPressureEvaluatorLinuxTest::kXdgPortalService =
    DbusMemoryPressureEvaluatorLinux::kXdgPortalService;
const char* const
    DbusMemoryPressureEvaluatorLinuxTest::kXdgPortalMemoryMonitorInterface =
        DbusMemoryPressureEvaluatorLinux::kXdgPortalMemoryMonitorInterface;

TEST_F(DbusMemoryPressureEvaluatorLinuxTest, Basic) {
  EXPECT_CALL(*mock_voter(), SetVote(base::MEMORY_PRESSURE_LEVEL_NONE, false))
      .WillOnce(Return());

  EmitLowMemoryWarning(0);
  EXPECT_EQ(evaluator()->current_vote(), base::MEMORY_PRESSURE_LEVEL_NONE);

  EXPECT_CALL(*mock_voter(),
              SetVote(base::MEMORY_PRESSURE_LEVEL_MODERATE, true))
      .Times(2)
      .WillRepeatedly(Return());

  EmitLowMemoryWarning(50);
  EXPECT_EQ(evaluator()->current_vote(), base::MEMORY_PRESSURE_LEVEL_MODERATE);

  EmitLowMemoryWarning(100);
  EXPECT_EQ(evaluator()->current_vote(), base::MEMORY_PRESSURE_LEVEL_MODERATE);

  EXPECT_CALL(*mock_voter(),
              SetVote(base::MEMORY_PRESSURE_LEVEL_CRITICAL, true))
      .WillOnce(Return());

  EmitLowMemoryWarning(255);
  EXPECT_EQ(evaluator()->current_vote(), base::MEMORY_PRESSURE_LEVEL_CRITICAL);
}

TEST_F(DbusMemoryPressureEvaluatorLinuxTest, PeriodicReset) {
  // Set the initial level.
  EmitLowMemoryWarning(100);
  EXPECT_EQ(evaluator()->current_vote(), base::MEMORY_PRESSURE_LEVEL_MODERATE);

  // Verify that the level will be reset after the time delta.
  EXPECT_CALL(*mock_voter(), SetVote(base::MEMORY_PRESSURE_LEVEL_NONE,
                                     /*notify_listeners=*/true));
  task_environment().FastForwardBy(kResetVotePeriod);
  EXPECT_EQ(evaluator()->current_vote(), base::MEMORY_PRESSURE_LEVEL_NONE);

  // Setting the level to NONE, while it is already NONE, will not notify
  // listeners.
  EXPECT_CALL(*mock_voter(), SetVote(base::MEMORY_PRESSURE_LEVEL_NONE,
                                     /*notify_listeners=*/false));
  EmitLowMemoryWarning(0);

  // Verify that the level will not be reset after the time delta when NONE.
  EXPECT_CALL(*mock_voter(), SetVote(base::MEMORY_PRESSURE_LEVEL_NONE, _))
      .Times(0);
  task_environment().FastForwardBy(kResetVotePeriod);
}

TEST_F(DbusMemoryPressureEvaluatorLinuxTest, PrefersConnectingToLmm) {
  EXPECT_CALL(*lmm_proxy(), ConnectToSignal(kLmmInterface, _, _, _)).Times(1);
  EXPECT_CALL(*portal_proxy(),
              ConnectToSignal(kXdgPortalMemoryMonitorInterface, _, _, _))
      .Times(0);

  AddRunningService(kLmmService);
  AddRunningService(kXdgPortalService);

  RunServiceChecks();
}

TEST_F(DbusMemoryPressureEvaluatorLinuxTest, FallsBackToPortal) {
  EXPECT_CALL(*lmm_proxy(), ConnectToSignal(kLmmInterface, _, _, _)).Times(0);
  EXPECT_CALL(*portal_proxy(),
              ConnectToSignal(kXdgPortalMemoryMonitorInterface, _, _, _))
      .Times(1);

  AddRunningService(kXdgPortalService);

  RunServiceChecks();
}
