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

#include "components/dbus/xdg/systemd.h"

#include <optional>

#include "base/environment.h"
#include "base/memory/scoped_refptr.h"
#include "base/scoped_environment_variable_override.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/nix/scoped_chrome_version_extra_override.h"
#include "base/version_info/channel.h"
#include "base/version_info/nix/version_extra_utils.h"
#include "components/dbus/xdg/request.h"
#include "components/dbus/xdg/systemd_constants.h"
#include "dbus/message.h"
#include "dbus/mock_bus.h"
#include "dbus/mock_object_proxy.h"
#include "dbus/object_path.h"
#include "dbus/object_proxy.h"
#include "dbus/property.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

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

namespace dbus_xdg {

namespace {

using internal::SystemdUnitStatus;

constexpr char kFakeUnitPath[] = "/fake/unit/path";

std::unique_ptr<dbus::Response> CreateActiveStateGetAllResponse(
    const std::string& state) {
  auto response = dbus::Response::CreateEmpty();
  dbus::MessageWriter writer(response.get());
  dbus::MessageWriter array_writer(nullptr);
  dbus::MessageWriter dict_entry_writer(nullptr);
  writer.OpenArray("{sv}", &array_writer);
  array_writer.OpenDictEntry(&dict_entry_writer);
  dict_entry_writer.AppendString(kSystemdActiveStateProp);
  dict_entry_writer.AppendVariantOfString(state);
  array_writer.CloseContainer(&dict_entry_writer);
  writer.CloseContainer(&array_writer);
  return response;
}

using SetSystemdScopeUnitNameForXdgPortalTest = ::testing::Test;

TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, NotNecessaryInFlatpak) {
  scoped_refptr<dbus::MockBus> bus =
      base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());

  base::ScopedEnvironmentVariableOverride env_override("FLATPAK_SANDBOX_DIR",
                                                       "/");

  std::optional<SystemdUnitStatus> status;

  internal::SetSystemdScopeUnitNameForXdgPortal(
      bus.get(), base::BindLambdaForTesting(
                     [&](SystemdUnitStatus result) { status = result; }));

  EXPECT_EQ(status, SystemdUnitStatus::kUnitNotNecessary);
}

TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, NotNecessaryInSnap) {
  scoped_refptr<dbus::MockBus> bus =
      base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());

  base::ScopedEnvironmentVariableOverride env_override("SNAP", "/");

  std::optional<SystemdUnitStatus> status;

  internal::SetSystemdScopeUnitNameForXdgPortal(
      bus.get(), base::BindLambdaForTesting(
                     [&](SystemdUnitStatus result) { status = result; }));

  EXPECT_EQ(status, SystemdUnitStatus::kUnitNotNecessary);
}

TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, NoSystemdService) {
  scoped_refptr<dbus::MockBus> bus =
      base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());

  auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));

  EXPECT_CALL(
      *bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
      .WillRepeatedly(Return(mock_dbus_proxy.get()));

  EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        auto response = dbus::Response::CreateEmpty();
        dbus::MessageWriter writer(response.get());
        writer.AppendBool(false);
        std::move(callback).Run(response.get());
      });

  std::optional<SystemdUnitStatus> status;

  internal::SetSystemdScopeUnitNameForXdgPortal(
      bus.get(), base::BindLambdaForTesting(
                     [&](SystemdUnitStatus result) { status = result; }));

  EXPECT_EQ(status, SystemdUnitStatus::kNoSystemdService);
}

TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, StartTransientUnitSuccess) {
  scoped_refptr<dbus::MockBus> bus =
      base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());

  auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));

  EXPECT_CALL(
      *bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
      .WillRepeatedly(Return(mock_dbus_proxy.get()));

  EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        auto response = dbus::Response::CreateEmpty();
        dbus::MessageWriter writer(response.get());
        writer.AppendBool(true);
        std::move(callback).Run(response.get());
      });

  auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));

  EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
                                   dbus::ObjectPath(kObjectPathSystemd)))
      .Times(2)
      .WillRepeatedly(Return(mock_systemd_proxy.get()));

  auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
  EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
                                   dbus::ObjectPath(kFakeUnitPath)))
      .WillOnce(Return(mock_dbus_unit_proxy.get()));

  EXPECT_CALL(*mock_systemd_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        // Expect kMethodStartTransientUnit first.
        EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
        EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);

        // Simulate a successful response
        auto response = dbus::Response::CreateEmpty();
        std::move(callback).Run(response.get());
      })
      .WillOnce([obj_path = kFakeUnitPath](
                    dbus::MethodCall* method_call, int timeout_ms,
                    dbus::ObjectProxy::ResponseCallback callback) {
        // Then expect kMethodGetUnit. A valid path must be provided.
        EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
        EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);

        // Simulate a successful response and provide a fake path to the object.
        auto response = dbus::Response::CreateEmpty();
        dbus::MessageWriter writer(response.get());
        writer.AppendObjectPath(dbus::ObjectPath(obj_path));
        std::move(callback).Run(response.get());
      });

  EXPECT_CALL(*mock_dbus_unit_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
        EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);
        // Simulate a successful response with "active" state.
        auto response = CreateActiveStateGetAllResponse(kSystemdStateActive);
        std::move(callback).Run(response.get());
      });

  std::optional<SystemdUnitStatus> status;

  internal::SetSystemdScopeUnitNameForXdgPortal(
      bus.get(), base::BindLambdaForTesting(
                     [&](SystemdUnitStatus result) { status = result; }));

  EXPECT_EQ(status, SystemdUnitStatus::kUnitStarted);
}

TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, StartTransientUnitFailure) {
  scoped_refptr<dbus::MockBus> bus =
      base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());

  auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));

  EXPECT_CALL(
      *bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
      .WillRepeatedly(Return(mock_dbus_proxy.get()));

  EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        auto response = dbus::Response::CreateEmpty();
        dbus::MessageWriter writer(response.get());
        writer.AppendBool(true);
        std::move(callback).Run(response.get());
      });

  auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));

  EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
                                   dbus::ObjectPath(kObjectPathSystemd)))
      .WillOnce(Return(mock_systemd_proxy.get()));

  EXPECT_CALL(*mock_systemd_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        // Simulate a failure by invoking the callback with nullptr
        std::move(callback).Run(nullptr);
      });

  std::optional<SystemdUnitStatus> status;

  internal::SetSystemdScopeUnitNameForXdgPortal(
      bus.get(), base::BindLambdaForTesting(
                     [&](SystemdUnitStatus result) { status = result; }));

  EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
}

TEST_F(SetSystemdScopeUnitNameForXdgPortalTest,
       StartTransientUnitInvalidUnitPath) {
  scoped_refptr<dbus::MockBus> bus =
      base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());

  auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));

  EXPECT_CALL(
      *bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
      .WillRepeatedly(Return(mock_dbus_proxy.get()));

  EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        auto response = dbus::Response::CreateEmpty();
        dbus::MessageWriter writer(response.get());
        writer.AppendBool(true);
        std::move(callback).Run(response.get());
      });

  auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));

  EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
                                   dbus::ObjectPath(kObjectPathSystemd)))
      .Times(2)
      .WillRepeatedly(Return(mock_systemd_proxy.get()));

  EXPECT_CALL(*mock_systemd_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
        EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);

        // Simulate a successful response
        auto response = dbus::Response::CreateEmpty();
        std::move(callback).Run(response.get());
      })
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
        EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);

        // Simulate a failure response.
        std::move(callback).Run(nullptr);
      });

  std::optional<SystemdUnitStatus> status;

  internal::SetSystemdScopeUnitNameForXdgPortal(
      bus.get(), base::BindLambdaForTesting(
                     [&](SystemdUnitStatus result) { status = result; }));

  EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
}

TEST_F(SetSystemdScopeUnitNameForXdgPortalTest,
       StartTransientUnitFailToActivate) {
  scoped_refptr<dbus::MockBus> bus =
      base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());

  auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));

  EXPECT_CALL(
      *bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
      .WillRepeatedly(Return(mock_dbus_proxy.get()));

  EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        auto response = dbus::Response::CreateEmpty();
        dbus::MessageWriter writer(response.get());
        writer.AppendBool(true);
        std::move(callback).Run(response.get());
      });

  auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));

  EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
                                   dbus::ObjectPath(kObjectPathSystemd)))
      .Times(2)
      .WillRepeatedly(Return(mock_systemd_proxy.get()));

  auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
  EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
                                   dbus::ObjectPath(kFakeUnitPath)))
      .WillOnce(Return(mock_dbus_unit_proxy.get()));

  EXPECT_CALL(*mock_systemd_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
        EXPECT_EQ(method_call->GetMember(), kMethodStartTransientUnit);

        // Simulate a successful response
        auto response = dbus::Response::CreateEmpty();
        std::move(callback).Run(response.get());
      })
      .WillOnce([obj_path = kFakeUnitPath](
                    dbus::MethodCall* method_call, int timeout_ms,
                    dbus::ObjectProxy::ResponseCallback callback) {
        EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
        EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);

        // Simulate a successful response
        auto response = dbus::Response::CreateEmpty();
        dbus::MessageWriter writer(response.get());
        writer.AppendObjectPath(dbus::ObjectPath(obj_path));
        std::move(callback).Run(response.get());
      });

  EXPECT_CALL(*mock_dbus_unit_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        // Then expect kMethodGetUnit. A valid path must be provided.
        EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
        EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);

        // Simulate a successful response, but with inactive state.
        auto response = CreateActiveStateGetAllResponse(kSystemdStateInactive);
        std::move(callback).Run(response.get());
      });

  std::optional<SystemdUnitStatus> status;

  internal::SetSystemdScopeUnitNameForXdgPortal(
      bus.get(), base::BindLambdaForTesting(
                     [&](SystemdUnitStatus result) { status = result; }));

  EXPECT_EQ(status, SystemdUnitStatus::kFailedToStart);
}

TEST_F(SetSystemdScopeUnitNameForXdgPortalTest, UnitNameConstruction) {
  scoped_refptr<dbus::MockBus> bus =
      base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());

  auto mock_dbus_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));

  EXPECT_CALL(
      *bus, GetObjectProxy(DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS)))
      .WillRepeatedly(Return(mock_dbus_proxy.get()));

  EXPECT_CALL(*mock_dbus_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        auto response = dbus::Response::CreateEmpty();
        dbus::MessageWriter writer(response.get());
        writer.AppendBool(true);
        std::move(callback).Run(response.get());
      });

  base::test::ScopedChromeVersionExtraOverride env_override(
      version_info::Channel::BETA);

  auto mock_systemd_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), kServiceNameSystemd, dbus::ObjectPath(kObjectPathSystemd));

  constexpr std::string_view kAppPrefix = "app-";
  constexpr std::string_view kScopeSuffix = ".scope";

  EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
                                   dbus::ObjectPath(kObjectPathSystemd)))
      .Times(2)
      .WillRepeatedly(Return(mock_systemd_proxy.get()));

  auto mock_dbus_unit_proxy = base::MakeRefCounted<dbus::MockObjectProxy>(
      bus.get(), kServiceNameSystemd, dbus::ObjectPath(kFakeUnitPath));
  EXPECT_CALL(*bus, GetObjectProxy(kServiceNameSystemd,
                                   dbus::ObjectPath(kFakeUnitPath)))
      .WillOnce(Return(mock_dbus_unit_proxy.get()));

  EXPECT_CALL(*mock_systemd_proxy, CallMethod(_, _, _))
      .WillOnce([&](dbus::MethodCall* method_call, int timeout_ms,
                    dbus::ObjectProxy::ResponseCallback callback) {
        dbus::MessageReader reader(method_call);
        std::string unit_name;
        EXPECT_TRUE(reader.PopString(&unit_name));

        // Check that the unit_name matches the expected format
        // Format: app-<ApplicationID>-<RANDOM>.scope
        EXPECT_TRUE(base::StartsWith(unit_name, kAppPrefix));
        EXPECT_TRUE(base::EndsWith(unit_name, kScopeSuffix));

        // Remove "app-" prefix and ".scope" suffix
        std::string name_body = unit_name.substr(
            kAppPrefix.size(),
            unit_name.size() - kAppPrefix.size() - kScopeSuffix.size());

        auto id_random = base::SplitString(
            name_body, "-", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
        ASSERT_EQ(id_random.size(), 2U);

        std::string application_id = id_random[0];
        EXPECT_FALSE(application_id.empty());
        EXPECT_TRUE(
            std::all_of(application_id.begin(), application_id.end(),
                        [](char c) { return std::isalnum(c) || c == '.'; }));

        std::string random_part = id_random[1];
        EXPECT_FALSE(random_part.empty());
        EXPECT_TRUE(std::all_of(random_part.begin(), random_part.end(),
                                [](char c) { return std::isalnum(c); }));

        auto response = dbus::Response::CreateEmpty();
        std::move(callback).Run(response.get());
      })
      .WillOnce([obj_path = kFakeUnitPath](
                    dbus::MethodCall* method_call, int timeout_ms,
                    dbus::ObjectProxy::ResponseCallback callback) {
        EXPECT_EQ(method_call->GetInterface(), kInterfaceSystemdManager);
        EXPECT_EQ(method_call->GetMember(), kMethodGetUnit);

        // Simulate a successful response
        auto response = dbus::Response::CreateEmpty();
        dbus::MessageWriter writer(response.get());
        writer.AppendObjectPath(dbus::ObjectPath(obj_path));
        std::move(callback).Run(response.get());
      });

  EXPECT_CALL(*mock_dbus_unit_proxy, CallMethod(_, _, _))
      .WillOnce([](dbus::MethodCall* method_call, int timeout_ms,
                   dbus::ObjectProxy::ResponseCallback callback) {
        // Then expect kMethodGetUnit. A valid path must be provided.
        EXPECT_EQ(method_call->GetInterface(), dbus::kPropertiesInterface);
        EXPECT_EQ(method_call->GetMember(), dbus::kPropertiesGetAll);

        // Simulate a successful response
        auto response = CreateActiveStateGetAllResponse(kSystemdStateActive);
        std::move(callback).Run(response.get());
      });

  std::optional<SystemdUnitStatus> status;

  internal::SetSystemdScopeUnitNameForXdgPortal(
      bus.get(), base::BindLambdaForTesting(
                     [&](SystemdUnitStatus result) { status = result; }));

  EXPECT_EQ(status, SystemdUnitStatus::kUnitStarted);
}

TEST(SystemdEscapeTest, EscapeSystemdUnitName) {
  EXPECT_EQ(internal::EscapeSystemdUnitName("foo-bar"), "foo\\x2dbar");
  EXPECT_EQ(internal::EscapeSystemdUnitName("foo/bar"), "foo-bar");
  EXPECT_EQ(internal::EscapeSystemdUnitName(".foo"), "\\x2efoo");
  EXPECT_EQ(internal::EscapeSystemdUnitName("1foo"), "1foo");
  EXPECT_EQ(internal::EscapeSystemdUnitName("foo_bar:baz.qux"),
            "foo_bar:baz.qux");
  EXPECT_EQ(internal::EscapeSystemdUnitName(""), "");
}

}  // namespace

}  // namespace dbus_xdg
