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

#include <stddef.h>
#include <stdint.h>

#include <iterator>
#include <memory>

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/api/hid/hid_device_manager.h"
#include "extensions/test/extension_test_message_listener.h"
#include "services/device/public/cpp/test/fake_hid_manager.h"
#include "services/device/public/cpp/test/hid_test_util.h"
#include "services/device/public/mojom/hid.mojom.h"

// This API is not supported on Android.
static_assert(!BUILDFLAG(IS_ANDROID));

namespace extensions {

namespace {

using ::device::FakeHidManager;

const char* const kTestDeviceGuids[] = {"A", "B", "C", "D", "E"};
const char* const kTestPhysicalDeviceIds[] = {"1", "2", "3", "4", "5"};

// These report descriptors define two devices with 8-byte input, output and
// feature reports. The first implements usage page 0xFF00 and has a single
// report without and ID. The second implements usage page 0xFF01 and has a
// single report with ID 1.
const uint8_t kReportDescriptor[] = {
    0x06, 0x00, 0xFF,  // Usage Page (Vendor Defined 0xFF00)
    0x08,              // Usage
    0xA1, 0x01,        // Collection (Application)
    0x15, 0x00,        //   Logical Minimum (0)
    0x26, 0xFF, 0x00,  //   Logical Maximum (255)
    0x75, 0x08,        //   Report Size (8)
    0x95, 0x08,        //   Report Count (8)
    0x08,              //   Usage
    0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,
                       //   No Null Position)
    0x08,              //   Usage
    0x91, 0x02,        //   Output (Data,Var,Abs,No Wrap,Linear,Preferred State,
                       //   No Null Position,Non-volatile)
    0x08,              //   Usage
    0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred
                       //   State,No Null Position,Non-volatile)
    0xC0,              // End Collection
};
const uint8_t kReportDescriptorWithIDs[] = {
    0x06, 0x01, 0xFF,  // Usage Page (Vendor Defined 0xFF01)
    0x08,              // Usage
    0xA1, 0x01,        // Collection (Application)
    0x15, 0x00,        //   Logical Minimum (0)
    0x26, 0xFF, 0x00,  //   Logical Maximum (255)
    0x85, 0x01,        //   Report ID (1)
    0x75, 0x08,        //   Report Size (8)
    0x95, 0x08,        //   Report Count (8)
    0x08,              //   Usage
    0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,
                       //   No Null Position)
    0x08,              //   Usage
    0x91, 0x02,        //   Output (Data,Var,Abs,No Wrap,Linear,Preferred State,
                       //   No Null Position,Non-volatile)
    0x08,              //   Usage
    0xB1, 0x02,        //   Feature (Data,Var,Abs,No Wrap,Linear,Preferred
                       //   State,No Null Position,Non-volatile)
    0xC0,              // End Collection
};

// Device IDs for the device granted permission by the manifest.
constexpr uint16_t kTestVendorId = 0x18D1;
constexpr uint16_t kTestProductId = 0x58F0;

class HidApiTest : public ExtensionApiTest {
 public:
  HidApiTest() {
    // Because Device Service also runs in this process (browser process), we
    // can set our binder to intercept requests for HidManager interface to it.
    fake_hid_manager_ = std::make_unique<FakeHidManager>();
    auto binder = base::BindRepeating(
        &FakeHidManager::Bind, base::Unretained(fake_hid_manager_.get()));
    HidDeviceManager::OverrideHidManagerBinderForTesting(binder);
  }

  ~HidApiTest() override {
    HidDeviceManager::OverrideHidManagerBinderForTesting(base::NullCallback());
  }

  void SetUpOnMainThread() override {
    ExtensionApiTest::SetUpOnMainThread();

    AddDevice(kTestDeviceGuids[0], kTestPhysicalDeviceIds[0], kTestVendorId,
              kTestProductId, false, "A");
    AddDevice(kTestDeviceGuids[1], kTestPhysicalDeviceIds[1], kTestVendorId,
              kTestProductId, true, "B");
    AddDevice(kTestDeviceGuids[2], kTestPhysicalDeviceIds[2], kTestVendorId,
              kTestProductId + 1, false, "C");
  }

  void AddDevice(const std::string& device_guid,
                 const std::string& physical_device_id,
                 int vendor_id,
                 int product_id,
                 bool report_id,
                 std::string serial_number) {
    std::vector<uint8_t> report_descriptor;
    if (report_id) {
      report_descriptor.insert(report_descriptor.begin(),
                               std::begin(kReportDescriptorWithIDs),
                               std::end(kReportDescriptorWithIDs));
    } else {
      report_descriptor.insert(report_descriptor.begin(),
                               std::begin(kReportDescriptor),
                               std::end(kReportDescriptor));
    }

    auto device = device::CreateDeviceFromReportDescriptor(
        vendor_id, product_id, report_descriptor);
    device->guid = device_guid;
    device->physical_device_id = physical_device_id;
    device->serial_number = serial_number;
    device->protected_input_report_ids = std::vector<uint8_t>{};
    device->protected_output_report_ids = std::vector<uint8_t>{};
    device->protected_feature_report_ids = std::vector<uint8_t>{};

    fake_hid_manager_->AddDevice(std::move(device));
  }

  FakeHidManager* GetFakeHidManager() { return fake_hid_manager_.get(); }

 protected:
  std::unique_ptr<FakeHidManager> fake_hid_manager_;
};

IN_PROC_BROWSER_TEST_F(HidApiTest, HidApp) {
  ASSERT_TRUE(RunExtensionTest("hid/api")) << message_;
}

IN_PROC_BROWSER_TEST_F(HidApiTest, OnDeviceAdded) {
  ExtensionTestMessageListener load_listener("loaded");
  ExtensionTestMessageListener result_listener("success");
  result_listener.set_failure_message("failure");

  ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("hid/add_event")));

  ASSERT_TRUE(load_listener.WaitUntilSatisfied());

  // Add a blocked device first so that the test will fail if a notification is
  // received.
  AddDevice(kTestDeviceGuids[3], kTestPhysicalDeviceIds[3], kTestVendorId,
            kTestProductId + 1, false, "A");
  AddDevice(kTestDeviceGuids[4], kTestPhysicalDeviceIds[4], kTestVendorId,
            kTestProductId, false, "A");
  ASSERT_TRUE(result_listener.WaitUntilSatisfied());
  EXPECT_EQ("success", result_listener.message());
}

IN_PROC_BROWSER_TEST_F(HidApiTest, OnDeviceRemoved) {
  ExtensionTestMessageListener load_listener("loaded");
  ExtensionTestMessageListener result_listener("success");
  result_listener.set_failure_message("failure");

  ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII("hid/remove_event")));
  ASSERT_TRUE(load_listener.WaitUntilSatisfied());

  // Device C was not returned by chrome.hid.getDevices, the app will not get
  // a notification.
  GetFakeHidManager()->RemoveDevice(kTestDeviceGuids[2]);
  // Device A was returned, the app will get a notification.
  GetFakeHidManager()->RemoveDevice(kTestDeviceGuids[0]);
  ASSERT_TRUE(result_listener.WaitUntilSatisfied());
  EXPECT_EQ("success", result_listener.message());
}

device::mojom::HidDeviceInfoPtr CreateDeviceWithOneCollection(
    const std::string& guid) {
  auto device_info = device::mojom::HidDeviceInfo::New();
  device_info->guid = guid;
  device_info->vendor_id = kTestVendorId;
  device_info->product_id = kTestProductId;
  auto collection = device::mojom::HidCollectionInfo::New();
  collection->usage =
      device::mojom::HidUsageAndPage::New(1, device::mojom::kPageVendor);
  auto report = device::mojom::HidReportDescription::New();
  collection->input_reports.push_back(std::move(report));
  device_info->collections.push_back(std::move(collection));
  return device_info;
}

device::mojom::HidDeviceInfoPtr CreateDeviceWithTwoCollections(
    const std::string guid) {
  auto device_info = CreateDeviceWithOneCollection(guid);
  auto collection = device::mojom::HidCollectionInfo::New();
  collection->usage =
      device::mojom::HidUsageAndPage::New(2, device::mojom::kPageVendor);
  collection->output_reports.push_back(
      device::mojom::HidReportDescription::New());
  device_info->collections.push_back(std::move(collection));
  return device_info;
}

IN_PROC_BROWSER_TEST_F(HidApiTest, DeviceAddedChangedRemoved) {
  constexpr char kTestGuid[] = "guid";

  ExtensionTestMessageListener load_listener("loaded");
  ExtensionTestMessageListener add_listener("added", ReplyBehavior::kWillReply);
  ExtensionTestMessageListener change_listener("changed");
  ExtensionTestMessageListener result_listener("success");
  result_listener.set_failure_message("failure");

  ASSERT_TRUE(
      LoadExtension(test_data_dir_.AppendASCII("hid/add_change_remove")));
  ASSERT_TRUE(load_listener.WaitUntilSatisfied());

  // Add a device with one collection.
  GetFakeHidManager()->AddDevice(CreateDeviceWithOneCollection(kTestGuid));
  ASSERT_TRUE(add_listener.WaitUntilSatisfied());

  // Update the device info to add a second collection. No event is generated,
  // so we will reply to the |add_listener| to signal to the test that the
  // change is complete.
  GetFakeHidManager()->ChangeDevice(CreateDeviceWithTwoCollections(kTestGuid));
  add_listener.Reply("device info updated");
  ASSERT_TRUE(change_listener.WaitUntilSatisfied());

  // Remove the device.
  GetFakeHidManager()->RemoveDevice(kTestGuid);
  ASSERT_TRUE(result_listener.WaitUntilSatisfied());
  EXPECT_EQ("success", result_listener.message());
}

}  // namespace
}  // namespace extensions
