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

#include "device/bluetooth/floss/floss_admin_client.h"

#include <map>
#include <utility>
#include <vector>

#include "base/compiler_specific.h"
#include "base/containers/to_vector.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "components/device_event_log/device_event_log.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/mock_bus.h"
#include "dbus/mock_exported_object.h"
#include "dbus/mock_object_proxy.h"
#include "dbus/object_path.h"
#include "device/bluetooth/floss/floss_dbus_client.h"
#include "device/bluetooth/floss/floss_manager_client.h"
#include "device/bluetooth/floss/test_helpers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace floss {
namespace {

using testing::_;
using testing::DoAll;

const uint32_t kTestCallbackId = 1000;
constexpr size_t kUUIDSize = 16;
const uint8_t kTestUuidInBytes[][kUUIDSize] = {
    {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
    {15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0},
};

const std::vector<device::BluetoothUUID> kTestUuidStr = {
    device::BluetoothUUID("00010203-0405-0607-0809-0a0b0c0d0e0f"),
    device::BluetoothUUID("0f0e0d0c-0b0a-0908-0706-050403020100"),
};

void FakeExportMethod(
    const std::string& interface_name,
    const std::string& method_name,
    const dbus::ExportedObject::MethodCallCallback& method_call_callback,
    dbus::ExportedObject::OnExportedCallback on_exported_callback) {
  std::move(on_exported_callback)
      .Run(interface_name, method_name, /*success=*/true);
}

}  // namespace

class FlossAdminClientTest : public testing::Test,
                             public FlossAdminClientObserver {
 public:
  FlossAdminClientTest() = default;

  base::Version GetCurrVersion() {
    return floss::version::GetMaximalSupportedVersion();
  }

  void SetUp() override {
    ::dbus::Bus::Options options;
    options.bus_type = ::dbus::Bus::BusType::SYSTEM;
    bus_ = base::MakeRefCounted<::dbus::MockBus>(std::move(options));
    client_ = FlossAdminClient::Create();
    client_->AddObserver(this);

    admin_path_ = FlossDBusClient::GenerateAdminPath(adapter_index_);
    callback_path_ = dbus::ObjectPath(FlossAdminClient::kExportedCallbacksPath);

    object_proxy_ = base::MakeRefCounted<::dbus::MockObjectProxy>(
        bus_.get(), kAdapterInterface, admin_path_);

    EXPECT_CALL(*bus_.get(), GetObjectProxy(kAdapterInterface, admin_path_))
        .WillRepeatedly(::testing::Return(object_proxy_.get()));
  }

  void TearDown() override {
    // Clean up the client first so it gets rid of all its references to the
    // various buses, object proxies, etc.
    client_.reset();
  }

  // AdminClientObserver overrides
  void DevicePolicyEffectChanged(
      const FlossDeviceId& device_id,
      const std::optional<PolicyEffect>& effect) override {
    fake_device_policy_effect_info_ = {device_id, effect};
  }

  void ServiceAllowlistChanged(
      const std::vector<device::BluetoothUUID>& allowlist) override {
    fake_service_allowlist_info_ = allowlist;
  }

  bool IsClientRegistered() { return client_->IsClientRegistered(); }

  void TestInit() {
    scoped_refptr<::dbus::MockExportedObject> exported_callback =
        base::MakeRefCounted<::dbus::MockExportedObject>(bus_.get(),
                                                         callback_path_);
    // Expected exported callbacks
    dbus::ExportedObject::MethodCallCallback
        method_handler_on_device_policy_effect_changed;
    EXPECT_CALL(*exported_callback.get(),
                ExportMethod(admin::kCallbackInterface,
                             admin::kOnDevicePolicyEffectChanged, _, _))
        .WillOnce(DoAll(testing::SaveArg<2>(
                            &method_handler_on_device_policy_effect_changed),
                        &FakeExportMethod));

    dbus::ExportedObject::MethodCallCallback
        method_handler_on_service_allowlist_changed;
    EXPECT_CALL(*exported_callback.get(),
                ExportMethod(admin::kCallbackInterface,
                             admin::kOnServiceAllowlistChanged, _, _))
        .WillOnce(DoAll(
            testing::SaveArg<2>(&method_handler_on_service_allowlist_changed),
            &FakeExportMethod));

    EXPECT_CALL(*bus_.get(), GetExportedObject(callback_path_))
        .WillRepeatedly(testing::Return(exported_callback.get()));

    // Expected call to RegisterAdminCallback when client is initialized
    EXPECT_CALL(*object_proxy_.get(),
                CallMethodWithErrorResponse(
                    HasMemberOf(admin::kRegisterCallback), _, _))
        .WillOnce([this](::dbus::MethodCall* method_call, int timeout_ms,
                         ::dbus::ObjectProxy::ResponseOrErrorCallback cb) {
          dbus::MessageReader msg(method_call);
          // D-Bus method call should have 1 parameter.
          dbus::ObjectPath param1;
          ASSERT_TRUE(msg.PopObjectPath(&param1));
          EXPECT_EQ(param1, this->callback_path_);
          EXPECT_FALSE(msg.HasMoreData());
          // Create a fake response with uint32_t return value.
          auto response = ::dbus::Response::CreateEmpty();
          dbus::MessageWriter writer(response.get());
          writer.AppendUint32(kTestCallbackId);
          std::move(cb).Run(response.get(), /*err=*/nullptr);
        });
    ASSERT_FALSE(IsClientRegistered());
    client_->Init(bus_.get(), kAdapterInterface, adapter_index_,
                  GetCurrVersion(), base::DoNothing());

    // Test exported callbacks are correctly parsed
    ASSERT_TRUE(!!method_handler_on_device_policy_effect_changed);
    ASSERT_TRUE(!!method_handler_on_service_allowlist_changed);
    ASSERT_TRUE(IsClientRegistered());

    // Expected call to UnregisterAdminCallback when client is destroyed
    EXPECT_CALL(*object_proxy_.get(),
                CallMethodWithErrorResponse(
                    HasMemberOf(admin::kUnregisterCallback), _, _))
        .WillOnce([](::dbus::MethodCall* method_call, int timeout_ms,
                     ::dbus::ObjectProxy::ResponseOrErrorCallback cb) {
          dbus::MessageReader msg(method_call);
          // D-Bus method call should have 1 parameter.
          uint32_t param1;
          ASSERT_TRUE(FlossDBusClient::ReadAllDBusParams(&msg, &param1));
          EXPECT_EQ(kTestCallbackId, param1);
          EXPECT_FALSE(msg.HasMoreData());
        });
  }

  void TestSetServiceAllowlist() {
    // Expected call to SetAllowedServices
    EXPECT_CALL(*object_proxy_.get(),
                CallMethodWithErrorResponse(
                    HasMemberOf(admin::kSetAllowedServices), _, _))
        .WillOnce([](::dbus::MethodCall* method_call, int timeout_ms,
                     ::dbus::ObjectProxy::ResponseOrErrorCallback cb) {
          dbus::MessageReader reader(method_call);
          dbus::MessageReader array_reader(nullptr);
          base::span<const uint8_t> buf;

          EXPECT_TRUE(reader.PopArray(&array_reader));

          for (const auto& uuid_in_bytes : kTestUuidInBytes) {
            EXPECT_TRUE(array_reader.PopArrayOfBytes(&buf));
            EXPECT_EQ(buf.size(), kUUIDSize);
            EXPECT_EQ(buf, uuid_in_bytes);
          }
          EXPECT_FALSE(reader.HasMoreData());
          EXPECT_FALSE(array_reader.HasMoreData());

          // Create a fake response with uint32_t return value.
          auto response = ::dbus::Response::CreateEmpty();
          dbus::MessageWriter writer(response.get());
          writer.AppendUint32(kTestCallbackId);
          std::move(cb).Run(response.get(), /*err=*/nullptr);
        });

    client_->SetAllowedServices(
        base::BindLambdaForTesting([](DBusResult<Void> ret) {}), kTestUuidStr);
  }

  void TestSetSimpleSecurePairingEnabled() {
    // Expected call to SetSimpleSecurePairingEnabled
    EXPECT_CALL(*object_proxy_.get(),
                CallMethodWithErrorResponse(
                    HasMemberOf(admin::kSetSimpleSecurePairingEnabled), _, _))
        .WillOnce([](::dbus::MethodCall* method_call, int timeout_ms,
                     ::dbus::ObjectProxy::ResponseOrErrorCallback cb) {
          dbus::MessageReader reader(method_call);
          bool enable;

          EXPECT_TRUE(reader.PopBool(&enable));
          EXPECT_TRUE(enable);

          // Create a fake response with uint32_t return value.
          auto response = ::dbus::Response::CreateEmpty();
          dbus::MessageWriter writer(response.get());
          writer.AppendUint32(kTestCallbackId);
          std::move(cb).Run(response.get(), /*err=*/nullptr);
        });

    client_->SetSimpleSecurePairingEnabled(
        base::BindLambdaForTesting([](DBusResult<Void> ret) {}), true);
  }

  int adapter_index_ = 5;
  dbus::ObjectPath admin_path_;
  dbus::ObjectPath callback_path_;

  scoped_refptr<::dbus::MockBus> bus_;
  scoped_refptr<::dbus::MockObjectProxy> object_proxy_;
  std::unique_ptr<FlossAdminClient> client_;

  // For observer test inspections.
  std::optional<std::tuple<FlossDeviceId, std::optional<PolicyEffect>>>
      fake_device_policy_effect_info_;
  std::vector<device::BluetoothUUID> fake_service_allowlist_info_;

  base::test::TaskEnvironment task_environment_;
  base::WeakPtrFactory<FlossAdminClientTest> weak_ptr_factory_{this};
};

TEST_F(FlossAdminClientTest, TestInitExportRegisterAdmin) {
  TestInit();
}

TEST_F(FlossAdminClientTest, TestSetServiceAllowlistBeforeInit) {
  TestSetServiceAllowlist();
  TestInit();
}

TEST_F(FlossAdminClientTest, TestSetServiceAllowlistAfterInit) {
  TestInit();
  TestSetServiceAllowlist();
}

TEST_F(FlossAdminClientTest, TestSetSimpleSecurePairingEnabledAfterInit) {
  TestInit();
  TestSetSimpleSecurePairingEnabled();
}

TEST_F(FlossAdminClientTest, TestSetMultiplePolicyAfterInit) {
  TestInit();
  TestSetServiceAllowlist();
  TestSetSimpleSecurePairingEnabled();
}

TEST_F(FlossAdminClientTest, TestSetMultiplePolicyBeforeInit) {
  TestSetServiceAllowlist();
  TestSetSimpleSecurePairingEnabled();
  TestInit();
}
}  // namespace floss
