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

#include <string.h>

#include <memory>

#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "content/public/test/browser_test.h"
#include "device/bluetooth/bluetooth_adapter.h"
#include "device/bluetooth/public/cpp/bluetooth_uuid.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/bluetooth/test/mock_bluetooth_device.h"
#include "extensions/browser/api/bluetooth/bluetooth_api.h"
#include "extensions/browser/api/bluetooth/bluetooth_event_router.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/common/extension_builder.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "testing/gmock/include/gmock/gmock.h"

static_assert(BUILDFLAG(IS_CHROMEOS));

using device::BluetoothAdapter;
using device::BluetoothDevice;
using device::BluetoothDeviceType;
using device::BluetoothDiscoverySession;
using device::BluetoothUUID;
using device::MockBluetoothAdapter;
using device::MockBluetoothDevice;

namespace utils = extensions::api_test_utils;
namespace api = extensions::api;

namespace extensions {
namespace {

using testing::_;
using testing::Invoke;

static const char* kAdapterAddress = "A1:A2:A3:A4:A5:A6";
static const char* kName = "whatsinaname";

class BluetoothApiTest : public ExtensionApiTest {
 public:
  BluetoothApiTest() = default;
  ~BluetoothApiTest() override = default;

  void SetUpOnMainThread() override {
    ExtensionApiTest::SetUpOnMainThread();
    empty_extension_ = ExtensionBuilder("Test").Build();
    SetUpMockAdapter();
  }

  void TearDownOnMainThread() override {
    EXPECT_CALL(*mock_adapter_, RemoveObserver(_));
  }

  void SetUpMockAdapter() {
    // The browser will clean this up when it is torn down
    mock_adapter_ = new testing::StrictMock<MockBluetoothAdapter>();
    event_router()->SetAdapterForTest(mock_adapter_);

    device1_ = std::make_unique<testing::NiceMock<MockBluetoothDevice>>(
        mock_adapter_, 0, "d1", "11:12:13:14:15:16", true /* paired */,
        true /* connected */);
    device2_ = std::make_unique<testing::NiceMock<MockBluetoothDevice>>(
        mock_adapter_, 0, "d2", "21:22:23:24:25:26", false /* paired */,
        false /* connected */);
    device3_ = std::make_unique<testing::NiceMock<MockBluetoothDevice>>(
        mock_adapter_, 0, "d3", "31:32:33:34:35:36", false /* paired */,
        false /* connected */);
  }

  void StartScanOverride(
      const device::BluetoothDiscoveryFilter* filter,
      base::OnceCallback<void(/*is_error*/ bool,
                              device::UMABluetoothDiscoverySessionOutcome)>&
          callback) {
    if (fail_next_call_) {
      std::move(callback).Run(
          true, device::UMABluetoothDiscoverySessionOutcome::UNKNOWN);
      fail_next_call_ = false;
      return;
    }
    std::move(callback).Run(
        false, device::UMABluetoothDiscoverySessionOutcome::SUCCESS);
  }

  void StopScanOverride(
      device::BluetoothAdapter::DiscoverySessionResultCallback callback) {
    if (fail_next_call_) {
      std::move(callback).Run(
          /*is_error=*/true,
          device::UMABluetoothDiscoverySessionOutcome::UNKNOWN);
      fail_next_call_ = false;
      return;
    }
    std::move(callback).Run(
        /*is_error=*/false,
        device::UMABluetoothDiscoverySessionOutcome::SUCCESS);
  }

  void FailNextCall() { fail_next_call_ = true; }

  template <class T>
  T* setupFunction(T* function) {
    function->set_extension(empty_extension_.get());
    function->set_has_callback(true);
    return function;
  }

 protected:
  raw_ptr<testing::StrictMock<MockBluetoothAdapter>,
          AcrossTasksDanglingUntriaged>
      mock_adapter_;
  std::unique_ptr<testing::NiceMock<MockBluetoothDevice>> device1_;
  std::unique_ptr<testing::NiceMock<MockBluetoothDevice>> device2_;
  std::unique_ptr<testing::NiceMock<MockBluetoothDevice>> device3_;

  BluetoothEventRouter* event_router() {
    return bluetooth_api()->event_router();
  }

  BluetoothAPI* bluetooth_api() { return BluetoothAPI::Get(profile()); }

 private:
  scoped_refptr<const Extension> empty_extension_;
  bool fail_next_call_ = false;
};

// The sanity test doesn't use the MockBluetoothAdapter.
using BluetoothApiSanityTest = ExtensionApiTest;

IN_PROC_BROWSER_TEST_F(BluetoothApiSanityTest, Basics) {
  ASSERT_TRUE(
      RunExtensionTest("bluetooth/basics", {.launch_as_platform_app = true}));
}

IN_PROC_BROWSER_TEST_F(BluetoothApiTest, GetAdapterState) {
  EXPECT_CALL(*mock_adapter_, GetAddress())
      .WillOnce(testing::Return(kAdapterAddress));
  EXPECT_CALL(*mock_adapter_, GetName())
      .WillOnce(testing::Return(kName));
  EXPECT_CALL(*mock_adapter_, IsPresent())
      .WillOnce(testing::Return(false));
  EXPECT_CALL(*mock_adapter_, IsPowered())
      .WillOnce(testing::Return(true));
  EXPECT_CALL(*mock_adapter_, IsDiscovering())
      .WillOnce(testing::Return(false));

  scoped_refptr<api::BluetoothGetAdapterStateFunction> get_adapter_state;
  get_adapter_state = setupFunction(new api::BluetoothGetAdapterStateFunction);

  std::optional<base::Value> result = utils::RunFunctionAndReturnSingleResult(
      get_adapter_state.get(), "[]", profile());
  ASSERT_TRUE(result);
  ASSERT_TRUE(result->is_dict());
  auto state = api::bluetooth::AdapterState::FromValue(result->GetDict());
  ASSERT_TRUE(state);

  EXPECT_FALSE(state->available);
  EXPECT_TRUE(state->powered);
  EXPECT_FALSE(state->discovering);
  EXPECT_EQ(kName, state->name);
  EXPECT_EQ(kAdapterAddress, state->address);
}

IN_PROC_BROWSER_TEST_F(BluetoothApiTest, DeviceEvents) {
  ResultCatcher catcher;
  catcher.RestrictToBrowserContext(profile());

  ASSERT_TRUE(LoadExtension(
        test_data_dir_.AppendASCII("bluetooth/device_events")));

  ExtensionTestMessageListener events_received("ready",
                                               ReplyBehavior::kWillReply);
  event_router()->DeviceAdded(mock_adapter_, device1_.get());
  event_router()->DeviceAdded(mock_adapter_, device2_.get());

  EXPECT_CALL(*device2_, GetName())
      .WillRepeatedly(
          testing::Return(std::optional<std::string>("the real d2")));
  EXPECT_CALL(*device2_, GetNameForDisplay())
      .WillRepeatedly(testing::Return(u"the real d2"));
  event_router()->DeviceChanged(mock_adapter_, device2_.get());

  event_router()->DeviceAdded(mock_adapter_, device3_.get());
  event_router()->DeviceRemoved(mock_adapter_, device1_.get());
  EXPECT_TRUE(events_received.WaitUntilSatisfied());
  events_received.Reply("go");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(BluetoothApiTest, Discovery) {
  // Simulate start discovery failure
  EXPECT_CALL(*mock_adapter_, StartScanWithFilter_(_, _))
      .WillOnce(Invoke(this, &BluetoothApiTest::StartScanOverride));
  FailNextCall();
  scoped_refptr<api::BluetoothStartDiscoveryFunction> start_function;
  start_function = setupFunction(new api::BluetoothStartDiscoveryFunction);
  std::string error(
      utils::RunFunctionAndReturnError(start_function.get(), "[]", profile()));

  testing::Mock::VerifyAndClearExpectations(mock_adapter_);
  // Simulate successful start discovery
  EXPECT_CALL(*mock_adapter_, StartScanWithFilter_(_, _))
      .WillOnce(Invoke(this, &BluetoothApiTest::StartScanOverride));
  start_function = setupFunction(new api::BluetoothStartDiscoveryFunction);
  utils::RunFunction(start_function.get(), "[]", profile(),
                     utils::FunctionMode::kNone);

  testing::Mock::VerifyAndClearExpectations(mock_adapter_);
  // Simulate stop discovery with a failure
  EXPECT_CALL(*mock_adapter_, StopScan(_))
      .WillOnce(Invoke(this, &BluetoothApiTest::StopScanOverride));
  FailNextCall();
  scoped_refptr<api::BluetoothStopDiscoveryFunction> stop_function;
  stop_function = setupFunction(new api::BluetoothStopDiscoveryFunction);
  [[maybe_unused]] auto result = utils::RunFunctionAndReturnSingleResult(
      stop_function.get(), "[]", profile());
  SetUpMockAdapter();
}

IN_PROC_BROWSER_TEST_F(BluetoothApiTest, DiscoveryCallback) {
  EXPECT_CALL(*mock_adapter_, StartScanWithFilter_(_, _))
      .WillOnce(Invoke(this, &BluetoothApiTest::StartScanOverride));
  EXPECT_CALL(*mock_adapter_, StopScan(_))
      .WillOnce(Invoke(this, &BluetoothApiTest::StopScanOverride));
  ResultCatcher catcher;
  catcher.RestrictToBrowserContext(profile());

  ExtensionTestMessageListener discovery_started("ready",
                                                 ReplyBehavior::kWillReply);
  ASSERT_TRUE(LoadExtension(
        test_data_dir_.AppendASCII("bluetooth/discovery_callback")));
  EXPECT_TRUE(discovery_started.WaitUntilSatisfied());

  event_router()->DeviceAdded(mock_adapter_, device1_.get());

  discovery_started.Reply("go");
  ExtensionTestMessageListener discovery_stopped("ready",
                                                 ReplyBehavior::kWillReply);
  EXPECT_CALL(*mock_adapter_, RemoveObserver(_));
  EXPECT_TRUE(discovery_stopped.WaitUntilSatisfied());

  SetUpMockAdapter();
  event_router()->DeviceAdded(mock_adapter_, device2_.get());
  discovery_stopped.Reply("go");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(BluetoothApiTest, DiscoveryInProgress) {
  EXPECT_CALL(*mock_adapter_, GetAddress())
      .WillOnce(testing::Return(kAdapterAddress));
  EXPECT_CALL(*mock_adapter_, GetName())
      .WillOnce(testing::Return(kName));
  EXPECT_CALL(*mock_adapter_, IsPresent())
      .WillOnce(testing::Return(true));
  EXPECT_CALL(*mock_adapter_, IsPowered())
      .WillOnce(testing::Return(true));

  // Fake that the adapter is discovering
  EXPECT_CALL(*mock_adapter_, IsDiscovering())
      .WillOnce(testing::Return(true));
  event_router()->AdapterDiscoveringChanged(mock_adapter_, true);

  // Cache a device before the extension starts discovering
  event_router()->DeviceAdded(mock_adapter_, device1_.get());

  ResultCatcher catcher;
  catcher.RestrictToBrowserContext(profile());

  EXPECT_CALL(*mock_adapter_, StartScanWithFilter_(_, _))
      .WillOnce(Invoke(this, &BluetoothApiTest::StartScanOverride));
  EXPECT_CALL(*mock_adapter_, StopScan(_))
      .WillOnce(Invoke(this, &BluetoothApiTest::StopScanOverride));

  ExtensionTestMessageListener discovery_started("ready",
                                                 ReplyBehavior::kWillReply);
  ASSERT_TRUE(LoadExtension(
        test_data_dir_.AppendASCII("bluetooth/discovery_in_progress")));
  EXPECT_TRUE(discovery_started.WaitUntilSatisfied());

  // Only this should be received. No additional notification should be sent for
  // devices discovered before the discovery session started.
  event_router()->DeviceAdded(mock_adapter_, device2_.get());

  discovery_started.Reply("go");
  ExtensionTestMessageListener discovery_stopped("ready",
                                                 ReplyBehavior::kWillReply);
  EXPECT_CALL(*mock_adapter_, RemoveObserver(_));
  EXPECT_TRUE(discovery_stopped.WaitUntilSatisfied());

  SetUpMockAdapter();
  // This should never be received.
  event_router()->DeviceAdded(mock_adapter_, device2_.get());
  discovery_stopped.Reply("go");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(BluetoothApiTest, OnAdapterStateChanged) {
  ResultCatcher catcher;
  catcher.RestrictToBrowserContext(profile());

  // Load and wait for setup
  ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
  ASSERT_TRUE(
      LoadExtension(
          test_data_dir_.AppendASCII("bluetooth/on_adapter_state_changed")));
  EXPECT_TRUE(listener.WaitUntilSatisfied());

  EXPECT_CALL(*mock_adapter_, GetAddress())
      .WillOnce(testing::Return(kAdapterAddress));
  EXPECT_CALL(*mock_adapter_, GetName())
      .WillOnce(testing::Return(kName));
  EXPECT_CALL(*mock_adapter_, IsPresent())
      .WillOnce(testing::Return(false));
  EXPECT_CALL(*mock_adapter_, IsPowered())
      .WillOnce(testing::Return(false));
  EXPECT_CALL(*mock_adapter_, IsDiscovering())
      .WillOnce(testing::Return(false));
  event_router()->AdapterPoweredChanged(mock_adapter_, false);

  EXPECT_CALL(*mock_adapter_, GetAddress())
      .WillOnce(testing::Return(kAdapterAddress));
  EXPECT_CALL(*mock_adapter_, GetName())
      .WillOnce(testing::Return(kName));
  EXPECT_CALL(*mock_adapter_, IsPresent())
      .WillOnce(testing::Return(true));
  EXPECT_CALL(*mock_adapter_, IsPowered())
      .WillOnce(testing::Return(true));
  EXPECT_CALL(*mock_adapter_, IsDiscovering())
      .WillOnce(testing::Return(true));
  event_router()->AdapterPresentChanged(mock_adapter_, true);

  EXPECT_CALL(*mock_adapter_, GetAddress())
      .WillOnce(testing::Return(kAdapterAddress));
  EXPECT_CALL(*mock_adapter_, GetName())
      .WillOnce(testing::Return(kName));
  EXPECT_CALL(*mock_adapter_, IsPresent())
      .WillOnce(testing::Return(true));
  EXPECT_CALL(*mock_adapter_, IsPowered())
      .WillOnce(testing::Return(true));
  EXPECT_CALL(*mock_adapter_, IsDiscovering())
      .WillOnce(testing::Return(true));
  event_router()->AdapterDiscoveringChanged(mock_adapter_, true);

  listener.Reply("go");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(BluetoothApiTest, GetDevices) {
  ResultCatcher catcher;
  catcher.RestrictToBrowserContext(profile());

  BluetoothAdapter::ConstDeviceList devices;
  devices.push_back(device1_.get());
  devices.push_back(device2_.get());

  EXPECT_CALL(*mock_adapter_, GetDevices())
      .Times(1)
      .WillRepeatedly(testing::Return(devices));

  // Load and wait for setup
  ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
  ASSERT_TRUE(
      LoadExtension(test_data_dir_.AppendASCII("bluetooth/get_devices")));
  EXPECT_TRUE(listener.WaitUntilSatisfied());

  listener.Reply("go");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(BluetoothApiTest, GetDevice) {
  ResultCatcher catcher;
  catcher.RestrictToBrowserContext(profile());

  EXPECT_CALL(*mock_adapter_, GetDevice(device1_->GetAddress()))
      .WillOnce(testing::Return(device1_.get()));
  EXPECT_CALL(*mock_adapter_, GetDevice(device2_->GetAddress()))
      .Times(1)
      .WillRepeatedly(testing::Return(static_cast<BluetoothDevice*>(nullptr)));

  // Load and wait for setup
  ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
  ASSERT_TRUE(
      LoadExtension(test_data_dir_.AppendASCII("bluetooth/get_device")));
  EXPECT_TRUE(listener.WaitUntilSatisfied());

  listener.Reply("go");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(BluetoothApiTest, DeviceInfo) {
  ResultCatcher catcher;
  catcher.RestrictToBrowserContext(profile());

  // Set up the first device object to reflect a real-world device.
  BluetoothAdapter::ConstDeviceList devices;

  EXPECT_CALL(*device1_, GetAddress())
      .WillRepeatedly(testing::Return("A4:17:31:00:00:00"));
  EXPECT_CALL(*device1_, GetName())
      .WillRepeatedly(
          testing::Return(std::optional<std::string>("Chromebook Pixel")));
  EXPECT_CALL(*device1_, GetNameForDisplay())
      .WillRepeatedly(testing::Return(u"Chromebook Pixel"));
  EXPECT_CALL(*device1_, GetBluetoothClass())
      .WillRepeatedly(testing::Return(0x080104));
  EXPECT_CALL(*device1_, GetDeviceType())
      .WillRepeatedly(testing::Return(BluetoothDeviceType::COMPUTER));
  EXPECT_CALL(*device1_, GetVendorIDSource())
      .WillRepeatedly(testing::Return(BluetoothDevice::VENDOR_ID_BLUETOOTH));
  EXPECT_CALL(*device1_, GetVendorID()).WillRepeatedly(testing::Return(0x00E0));
  EXPECT_CALL(*device1_, GetProductID())
      .WillRepeatedly(testing::Return(0x240A));
  EXPECT_CALL(*device1_, GetDeviceID()).WillRepeatedly(testing::Return(0x0400));

  BluetoothDevice::UUIDSet uuids;
  uuids.insert(BluetoothUUID("1105"));
  uuids.insert(BluetoothUUID("1106"));

  EXPECT_CALL(*device1_, GetUUIDs()).WillOnce(testing::Return(uuids));

  devices.push_back(device1_.get());

  // Leave the second largely empty so we can check a device without
  // available information.
  devices.push_back(device2_.get());

  EXPECT_CALL(*mock_adapter_, GetDevices())
      .Times(1)
      .WillRepeatedly(testing::Return(devices));

  // Load and wait for setup
  ExtensionTestMessageListener listener("ready", ReplyBehavior::kWillReply);
  ASSERT_TRUE(
      LoadExtension(test_data_dir_.AppendASCII("bluetooth/device_info")));
  EXPECT_TRUE(listener.WaitUntilSatisfied());

  listener.Reply("go");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

}  // namespace
}  // namespace extensions
