// Copyright 2019 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/ui/hid/hid_chooser_controller.h"

#include <array>
#include <memory>
#include <set>
#include <string>
#include <utility>

#include "base/command_line.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chrome/browser/hid/hid_chooser_context.h"
#include "chrome/browser/hid/hid_chooser_context_factory.h"
#include "chrome/browser/hid/mock_hid_device_observer.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_profile.h"
#include "components/permissions/mock_chooser_controller_view.h"
#include "content/public/browser/hid_chooser.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/web_contents_tester.h"
#include "services/device/public/cpp/hid/hid_switches.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/cpp/test/test_report_descriptors.h"
#include "services/device/public/mojom/hid.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/resource/resource_bundle.h"
#include "url/gurl.h"

using ::base::test::RunClosure;
using ::base::test::TestFuture;
using ::testing::_;

namespace {

constexpr char kDefaultTestUrl[] = "https://www.google.com/";

constexpr auto kTestPhysicalDeviceIds =
    std::to_array<const char*>({"1", "2", "3"});

constexpr uint16_t kVendorYubico = 0x1050;
constexpr uint16_t kProductYubicoGnubby = 0x0200;
constexpr uint16_t kUsageFidoU2f = 0x0001;

class HidChooserControllerTest : public ChromeRenderViewHostTestHarness {
 public:
  HidChooserControllerTest() = default;
  HidChooserControllerTest(HidChooserControllerTest&) = delete;
  HidChooserControllerTest& operator=(HidChooserControllerTest&) = delete;
  ~HidChooserControllerTest() override = default;

  permissions::MockChooserControllerView& view() {
    return mock_chooser_controller_view_;
  }
  MockHidDeviceObserver& device_observer() { return mock_device_observer_; }

  void SetUp() override {
    ChromeRenderViewHostTestHarness::SetUp();

    content::WebContentsTester* web_contents_tester =
        content::WebContentsTester::For(web_contents());
    web_contents_tester->NavigateAndCommit(GURL(kDefaultTestUrl));

    // Set fake HID manager for HidChooserContext.
    mojo::PendingRemote<device::mojom::HidManager> hid_manager;
    hid_manager_.Bind(hid_manager.InitWithNewPipeAndPassReceiver());
    base::RunLoop run_loop;
    auto* chooser_context = HidChooserContextFactory::GetForProfile(profile());
    chooser_context->SetHidManagerForTesting(
        std::move(hid_manager),
        base::BindLambdaForTesting(
            [&run_loop](std::vector<device::mojom::HidDeviceInfoPtr> devices) {
              run_loop.Quit();
            }));
    run_loop.Run();

    chooser_context->AddDeviceObserver(&mock_device_observer_);
  }

  void TearDown() override {
    auto* chooser_context = HidChooserContextFactory::GetForProfile(profile());
    chooser_context->RemoveDeviceObserver(&mock_device_observer_);

    ChromeRenderViewHostTestHarness::TearDown();
  }

  std::unique_ptr<HidChooserController> CreateHidChooserController(
      std::vector<blink::mojom::HidDeviceFilterPtr> filters,
      std::vector<blink::mojom::HidDeviceFilterPtr> exclusion_filters = {},
      content::HidChooser::Callback callback = base::DoNothing()) {
    auto hid_chooser_controller = std::make_unique<HidChooserController>(
        main_rfh(), std::move(filters), std::move(exclusion_filters),
        std::move(callback));
    hid_chooser_controller->set_view(&mock_chooser_controller_view_);
    return hid_chooser_controller;
  }

  device::mojom::HidDeviceInfoPtr CreateAndAddFakeHidDevice(
      const std::string& physical_device_id,
      uint32_t vendor_id,
      uint16_t product_id,
      const std::string& product_string,
      const std::string& serial_number,
      uint16_t usage_page = device::mojom::kPageGenericDesktop,
      uint16_t usage = device::mojom::kGenericDesktopGamePad) {
    return hid_manager_.CreateAndAddDeviceWithTopLevelUsage(
        physical_device_id, vendor_id, product_id, product_string,
        serial_number, device::mojom::HidBusType::kHIDBusTypeUSB, usage_page,
        usage);
  }

  void ConnectDevice(const device::mojom::HidDeviceInfo& device) {
    hid_manager_.AddDevice(device.Clone());
  }

  void DisconnectDevice(const device::mojom::HidDeviceInfo& device) {
    hid_manager_.RemoveDevice(device.guid);
  }

  void UpdateDevice(const device::mojom::HidDeviceInfo& device) {
    hid_manager_.ChangeDevice(device.Clone());
  }

  blink::mojom::DeviceIdFilterPtr CreateVendorFilter(uint16_t vendor_id) {
    return blink::mojom::DeviceIdFilter::NewVendor(vendor_id);
  }

  blink::mojom::DeviceIdFilterPtr CreateVendorAndProductFilter(
      uint16_t vendor_id,
      uint16_t product_id) {
    return blink::mojom::DeviceIdFilter::NewVendorAndProduct(
        blink::mojom::VendorAndProduct::New(vendor_id, product_id));
  }

  blink::mojom::UsageFilterPtr CreatePageFilter(uint16_t usage_page) {
    return blink::mojom::UsageFilter::NewPage(usage_page);
  }

  blink::mojom::UsageFilterPtr CreateUsageAndPageFilter(uint16_t usage_page,
                                                        uint16_t usage) {
    return blink::mojom::UsageFilter::NewUsageAndPage(
        device::mojom::HidUsageAndPage::New(usage, usage_page));
  }

 private:
  device::FakeHidManager hid_manager_;
  permissions::MockChooserControllerView mock_chooser_controller_view_;
  MockHidDeviceObserver mock_device_observer_;
};

}  // namespace

TEST_F(HidChooserControllerTest, EmptyChooser) {
  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // Create the HidChooserController. There should be no options.
  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  EXPECT_EQ(0u, hid_chooser_controller->NumOptions());
}

TEST_F(HidChooserControllerTest, AddBlockedFidoDevice) {
  base::RunLoop device_added_loop;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device blocked by vendor/product ID.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], kVendorYubico,
                            kProductYubicoGnubby, "gnubby", "001");
  device_added_loop.Run();

  // 2. Create the HidChooserController. The blocked device should be excluded.
  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  EXPECT_EQ(0u, hid_chooser_controller->NumOptions());
}

TEST_F(HidChooserControllerTest, AddUnknownFidoDevice) {
  // Devices that expose a top-level collection with the FIDO usage page should
  // be blocked even if they aren't on the HID blocklist.
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device blocked by HID usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "fido", "001",
                            device::mojom::kPageFido, kUsageFidoU2f);
  device_added_loop1.Run();

  // 2. Connect a second device blocked by HID usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "fido", "002",
                            device::mojom::kPageFido, 0);
  device_added_loop2.Run();

  // 3. Create the HidChooserController. The blocked devices should be excluded.
  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  EXPECT_EQ(0u, hid_chooser_controller->NumOptions());
}

// Test fixture that disables the HID blocklist. This has to be done during test
// setup, rather than the test body, to avoid modifications of the (globally
// shared) CommandLine racing against other threads using it.
class HidChooserControllerWithoutBlocklistTest
    : public HidChooserControllerTest {
 public:
  HidChooserControllerWithoutBlocklistTest() {
    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        switches::kDisableHidBlocklist);
  }
  ~HidChooserControllerWithoutBlocklistTest() override = default;
};

TEST_F(HidChooserControllerWithoutBlocklistTest,
       BlockedFidoDeviceAllowedWithFlag) {
  base::RunLoop device_added_loop;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device with the FIDO usage page. The device uses
  // vendor/product IDs that are blocked by the HID blocklist.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], kVendorYubico,
                            kProductYubicoGnubby, "gnubby", "001",
                            device::mojom::kPageFido, kUsageFidoU2f);
  device_added_loop.Run();

  // 2. Create the HidChooserController. The blocked device should be included.
  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"gnubby", hid_chooser_controller->GetOption(0));
}

TEST_F(HidChooserControllerTest, AddNamedDevice) {
  base::RunLoop device_added_loop;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device with a non-empty product name string.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001");
  device_added_loop.Run();

  // 2. Create the HidChooserController. The option text should include the
  // product name.
  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));
}

TEST_F(HidChooserControllerTest, AddUnnamedDevice) {
  base::RunLoop device_added_loop;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device with an empty product name string.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "", "001");
  device_added_loop.Run();

  // 2. Create the HidChooserController. The option text should use an alternate
  // string.
  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"Unknown Device (0001:0001)",
            hid_chooser_controller->GetOption(0));
}

TEST_F(HidChooserControllerTest, DeviceIdFilterVendorOnly) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001");
  device_added_loop1.Run();

  // 2. Connect a second device with the same vendor ID.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 1, 2, "b", "002");
  device_added_loop2.Run();

  // 3. Connect a device with a different vendor ID.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 2, 2, "c", "003");
  device_added_loop3.Run();

  // 4. Create the HidChooserController with a vendor ID filter. The third
  // device should be excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> filters;
  filters.push_back(
      blink::mojom::HidDeviceFilter::New(CreateVendorFilter(1), nullptr));
  auto hid_chooser_controller = CreateHidChooserController(std::move(filters));
  options_initialized_loop.Run();

  EXPECT_EQ(2u, hid_chooser_controller->NumOptions());

  std::set<std::u16string> options{hid_chooser_controller->GetOption(0),
                                   hid_chooser_controller->GetOption(1)};
  EXPECT_THAT(options, testing::UnorderedElementsAre(u"a", u"b"));
}

TEST_F(HidChooserControllerTest, DeviceIdFilterVendorAndProduct) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001");
  device_added_loop1.Run();

  // 2. Connect a device with matching vendor ID but non-matching product ID.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 1, 2, "b", "002");
  device_added_loop2.Run();

  // 3. Connect a device with non-matching vendor and product IDs.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 2, 2, "c", "003");
  device_added_loop3.Run();

  // 4. Create the HidChooserController with a vendor and product ID filter. The
  // second and third devices should be excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> filters;
  filters.push_back(blink::mojom::HidDeviceFilter::New(
      CreateVendorAndProductFilter(1, 1), nullptr));
  auto hid_chooser_controller = CreateHidChooserController(std::move(filters));
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));
}

TEST_F(HidChooserControllerTest, UsageFilterUsagePageOnly) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop1.Run();

  // 2. Connect a device with a different top-level usage page.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "b", "002",
                            device::mojom::kPageSimulation, 5);
  device_added_loop2.Run();

  // 3. Create the HidChooserController with a usage page filter. The second
  // device should be excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> filters;
  filters.push_back(blink::mojom::HidDeviceFilter::New(
      nullptr, CreatePageFilter(device::mojom::kPageGenericDesktop)));
  auto hid_chooser_controller = CreateHidChooserController(std::move(filters));
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));
}

TEST_F(HidChooserControllerTest, UsageFilterUsageAndPage) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop1.Run();

  // 2. Connect a device with matching usage page but non-matching usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "b", "002",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopKeyboard);
  device_added_loop2.Run();

  // 3. Connect a device with non-matching usage page and usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 3, 3, "c", "003",
                            device::mojom::kPageSimulation, 5);
  device_added_loop3.Run();

  // 4. Create the HidChooserController with a usage page and usage filter. The
  // second and third devices should be excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> filters;
  filters.push_back(blink::mojom::HidDeviceFilter::New(
      nullptr,
      CreateUsageAndPageFilter(device::mojom::kPageGenericDesktop,
                               device::mojom::kGenericDesktopGamePad)));
  auto hid_chooser_controller = CreateHidChooserController(std::move(filters));
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));
}

TEST_F(HidChooserControllerTest, DeviceIdAndUsageFilterIntersection) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop1.Run();

  // 2. Connect a device with matching usage page and usage but non-matching
  // vendor and product IDs.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "b", "002",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop2.Run();

  // 3. Connect a device with matching vendor and product IDs but non-matching
  // usage page and usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 1, 1, "c", "003",
                            device::mojom::kPageSimulation, 5);
  device_added_loop3.Run();

  // 4. Create the HidChooserController with a filter that tests vendor ID,
  // product ID, usage page, and usage. The second and third devices should be
  // excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> filters;
  filters.push_back(blink::mojom::HidDeviceFilter::New(
      CreateVendorAndProductFilter(1, 1),
      CreateUsageAndPageFilter(device::mojom::kPageGenericDesktop,
                               device::mojom::kGenericDesktopGamePad)));
  auto hid_chooser_controller = CreateHidChooserController(std::move(filters));
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));
}

TEST_F(HidChooserControllerTest, DeviceIdAndUsageFilterUnion) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop1.Run();

  // 2. Connect a device with matching usage page and usage but non-matching
  // vendor and product IDs.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "b", "002",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop2.Run();

  // 3. Connect a device with matching vendor and product IDs but non-matching
  // usage page and usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 1, 1, "c", "003",
                            device::mojom::kPageSimulation, 5);
  device_added_loop3.Run();

  // 4. Create the HidChooserController with a vendor/product ID filter and a
  // usage page/usage filter. No devices should be excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> filters;
  filters.push_back(blink::mojom::HidDeviceFilter::New(
      CreateVendorAndProductFilter(1, 1), nullptr));
  filters.push_back(blink::mojom::HidDeviceFilter::New(
      nullptr,
      CreateUsageAndPageFilter(device::mojom::kPageGenericDesktop,
                               device::mojom::kGenericDesktopGamePad)));
  auto hid_chooser_controller = CreateHidChooserController(std::move(filters));
  options_initialized_loop.Run();

  EXPECT_EQ(3u, hid_chooser_controller->NumOptions());
}

TEST_F(HidChooserControllerTest, ExcludeDeviceIdFilterVendorOnly) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001");
  device_added_loop1.Run();

  // 2. Connect a second device with a different vendor ID.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 1, "b", "002");
  device_added_loop2.Run();

  // 3. Connect a third device with the same vendor ID as second device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 2, 2, "c", "003");
  device_added_loop3.Run();

  // 4. Create the HidChooserController with a vendor ID exclusion filter. The
  // first device should be excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> exclusion_filters;
  exclusion_filters.push_back(
      blink::mojom::HidDeviceFilter::New(CreateVendorFilter(1), nullptr));
  auto hid_chooser_controller =
      CreateHidChooserController({}, std::move(exclusion_filters));
  options_initialized_loop.Run();

  EXPECT_EQ(2u, hid_chooser_controller->NumOptions());

  std::set<std::u16string> options{hid_chooser_controller->GetOption(0),
                                   hid_chooser_controller->GetOption(1)};
  EXPECT_THAT(options, testing::UnorderedElementsAre(u"b", u"c"));
}

TEST_F(HidChooserControllerTest, ExcludeDeviceIdFilterVendorAndProduct) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device with non-matching product ID.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001");
  device_added_loop1.Run();

  // 2. Connect a device with matching vendor and product IDs.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 1, 2, "b", "002");
  device_added_loop2.Run();

  // 3. Connect a device with non-matching vendor ID.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 2, 2, "c", "003");
  device_added_loop3.Run();

  // 4. Create the HidChooserController with a vendor and product ID exclusion
  // filter. The second device should be excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> exclusion_filters;
  exclusion_filters.push_back(blink::mojom::HidDeviceFilter::New(
      CreateVendorAndProductFilter(1, 2), nullptr));
  auto hid_chooser_controller =
      CreateHidChooserController({}, std::move(exclusion_filters));
  options_initialized_loop.Run();

  EXPECT_EQ(2u, hid_chooser_controller->NumOptions());

  std::set<std::u16string> options{hid_chooser_controller->GetOption(0),
                                   hid_chooser_controller->GetOption(1)};
  EXPECT_THAT(options, testing::UnorderedElementsAre(u"a", u"c"));
}

TEST_F(HidChooserControllerTest, ExcludeUsageFilterUsagePageOnly) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop1.Run();

  // 2. Connect a device with a different top-level usage page.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "b", "002",
                            device::mojom::kPageSimulation,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop2.Run();

  // 3. Connect a third device with the same top-level usage page as second
  // device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 3, 3, "c", "003",
                            device::mojom::kPageSimulation,
                            device::mojom::kGenericDesktopJoystick);
  device_added_loop3.Run();

  // 4. Create the HidChooserController with a usage page exclusion filter. The
  // first device should be excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> exclusion_filters;
  exclusion_filters.push_back(blink::mojom::HidDeviceFilter::New(
      nullptr, CreatePageFilter(device::mojom::kPageGenericDesktop)));
  auto hid_chooser_controller =
      CreateHidChooserController({}, std::move(exclusion_filters));
  options_initialized_loop.Run();

  EXPECT_EQ(2u, hid_chooser_controller->NumOptions());

  std::set<std::u16string> options{hid_chooser_controller->GetOption(0),
                                   hid_chooser_controller->GetOption(1)};
  EXPECT_THAT(options, testing::UnorderedElementsAre(u"b", u"c"));
}

TEST_F(HidChooserControllerTest, ExcludeUsageFilterUsageAndPage) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device with matching usage page but non-matching usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop1.Run();

  // 2. Connect a device with matching usage page and usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "b", "002",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopKeyboard);
  device_added_loop2.Run();

  // 3. Connect a device with non-matching usage page and usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 3, 3, "c", "003",
                            device::mojom::kPageSimulation, 5);
  device_added_loop3.Run();

  // 4. Create the HidChooserController with a usage page and usage filter. The
  // second device should be excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> exclusion_filters;
  exclusion_filters.push_back(blink::mojom::HidDeviceFilter::New(
      nullptr,
      CreateUsageAndPageFilter(device::mojom::kPageGenericDesktop,
                               device::mojom::kGenericDesktopKeyboard)));
  auto hid_chooser_controller =
      CreateHidChooserController({}, std::move(exclusion_filters));
  options_initialized_loop.Run();

  EXPECT_EQ(2u, hid_chooser_controller->NumOptions());

  std::set<std::u16string> options{hid_chooser_controller->GetOption(0),
                                   hid_chooser_controller->GetOption(1)};
  EXPECT_THAT(options, testing::UnorderedElementsAre(u"a", u"c"));
}

TEST_F(HidChooserControllerTest, ExcludeMatchingDeviceIdFilterVendorOnly) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device with non-matching product ID.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001");
  device_added_loop1.Run();

  // 2. Connect a device with matching vendor and product IDs.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 1, 2, "b", "002");
  device_added_loop2.Run();

  // 3. Connect a device with non-matching vendor and product IDs.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 2, 2, "c", "003");
  device_added_loop3.Run();

  // 4. Create the HidChooserController with a vendor ID filter and a vendor and
  // product ID exclusion filter. The second and third devices should be
  // excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> filters;
  filters.push_back(
      blink::mojom::HidDeviceFilter::New(CreateVendorFilter(1), nullptr));
  std::vector<blink::mojom::HidDeviceFilterPtr> exclusion_filters;
  exclusion_filters.push_back(blink::mojom::HidDeviceFilter::New(
      CreateVendorAndProductFilter(1, 2), nullptr));
  auto hid_chooser_controller = CreateHidChooserController(
      std::move(filters), std::move(exclusion_filters));
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));
}

TEST_F(HidChooserControllerTest, ExcludeMatchingUsageFilterPageOnly) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device with matching page and usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop1.Run();

  // 2. Connect a device with non-matching usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 2, 2, "b", "002",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopJoystick);
  device_added_loop2.Run();

  // 3. Connect a device with non-matching page and usage.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[2], 3, 3, "c", "003",
                            device::mojom::kPageSimulation,
                            device::mojom::kGenericDesktopJoystick);
  device_added_loop3.Run();

  // 4. Create the HidChooserController with a page filter and a usage and page
  // exclusion filter. The second and third devices should be excluded.
  std::vector<blink::mojom::HidDeviceFilterPtr> filters;
  filters.push_back(blink::mojom::HidDeviceFilter::New(
      nullptr, CreatePageFilter(device::mojom::kPageGenericDesktop)));
  std::vector<blink::mojom::HidDeviceFilterPtr> exclusion_filters;
  exclusion_filters.push_back(blink::mojom::HidDeviceFilter::New(
      nullptr,
      CreateUsageAndPageFilter(device::mojom::kPageGenericDesktop,
                               device::mojom::kGenericDesktopJoystick)));

  auto hid_chooser_controller = CreateHidChooserController(
      std::move(filters), std::move(exclusion_filters));
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));
}

TEST_F(HidChooserControllerTest, OneOptionForSamePhysicalDevice) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  base::MockCallback<content::HidChooser::Callback> callback;
  base::RunLoop callback_loop;
  std::vector<device::mojom::HidDeviceInfoPtr> devices;
  EXPECT_CALL(callback, Run(_))
      .WillOnce([&devices, &callback_loop](
                    std::vector<device::mojom::HidDeviceInfoPtr> d) {
        devices = std::move(d);
        callback_loop.Quit();
      });

  // 1. Connect a device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop1.Run();

  // 2. Connect a second device with the same physical device ID.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                            device::mojom::kPageSimulation, 5);
  device_added_loop2.Run();

  // 3. Create the HidChooserController and register a callback to get the
  // returned device list. There should be a single chooser option representing
  // both devices.
  auto hid_chooser_controller =
      CreateHidChooserController({}, {}, callback.Get());
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));

  // 4. Select the chooser option. The returned device list should include both
  // devices.
  hid_chooser_controller->Select({0});
  callback_loop.Run();

  EXPECT_EQ(2u, devices.size());
  EXPECT_EQ(kTestPhysicalDeviceIds[0], devices[0]->physical_device_id);
  EXPECT_EQ(kTestPhysicalDeviceIds[0], devices[1]->physical_device_id);
  EXPECT_NE(devices[0]->guid, devices[1]->guid);

  // Regression test for https://crbug.com/40683822. Ensure that the
  // set of options is still valid after the callback is run.
  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));
}

TEST_F(HidChooserControllerTest, NoMergeWithDifferentPhysicalDeviceIds) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop1.Run();

  // 2. Connect a second device with the same info as the first device except
  // for the physical device ID.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[1], 1, 1, "a", "001",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop2.Run();

  // 3. Create the HidChooserController. The devices should have separate
  // chooser options.
  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  EXPECT_EQ(2u, hid_chooser_controller->NumOptions());
}

TEST_F(HidChooserControllerTest, NoMergeWithEmptyPhysicalDeviceId) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // 1. Connect a device with an empty physical device ID.
  CreateAndAddFakeHidDevice("", 1, 1, "a", "001",
                            device::mojom::kPageGenericDesktop,
                            device::mojom::kGenericDesktopGamePad);
  device_added_loop1.Run();

  // 2. Connect a second device with an empty physical device ID.
  CreateAndAddFakeHidDevice("", 1, 1, "a", "001",
                            device::mojom::kPageSimulation, 5);
  device_added_loop2.Run();

  // 3. Create the HidChooserController. The devices should have separate
  // chooser options.
  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  EXPECT_EQ(2u, hid_chooser_controller->NumOptions());
}

TEST_F(HidChooserControllerTest, DeviceConnectAddsOption) {
  EXPECT_CALL(device_observer(), OnDeviceAdded(_));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  base::RunLoop option_added_loop;
  EXPECT_CALL(view(), OnOptionAdded(0))
      .WillOnce(RunClosure(option_added_loop.QuitClosure()));

  // 1. Create the HidChooserController.
  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  EXPECT_EQ(0u, hid_chooser_controller->NumOptions());

  // 2. Connect a device and verify a chooser option is added.
  auto device =
      CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                                device::mojom::kPageGenericDesktop,
                                device::mojom::kGenericDesktopGamePad);
  option_added_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
}

TEST_F(HidChooserControllerTest, DeviceDisconnectRemovesOption) {
  base::RunLoop device_added_loop;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop.QuitClosure()));

  EXPECT_CALL(device_observer(), OnDeviceRemoved(_));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  base::RunLoop option_removed_loop;
  EXPECT_CALL(view(), OnOptionRemoved(0))
      .WillOnce(RunClosure(option_removed_loop.QuitClosure()));

  // 1. Connect a device.
  auto device =
      CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001",
                                device::mojom::kPageGenericDesktop,
                                device::mojom::kGenericDesktopGamePad);
  device_added_loop.Run();

  // 2. Create the HidChooserController and verify that the device is included.
  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());

  // 3. Disconnect the device and verify the chooser option is removed.
  DisconnectDevice(*device);
  option_removed_loop.Run();

  EXPECT_EQ(0u, hid_chooser_controller->NumOptions());
}

namespace {

device::mojom::HidDeviceInfoPtr CreateDeviceWithOneCollection(
    const std::string& guid) {
  auto device_info = device::mojom::HidDeviceInfo::New();
  device_info->guid = guid;
  device_info->physical_device_id = "physical-device-id";
  device_info->vendor_id = 1;
  device_info->product_id = 1;
  device_info->product_name = "a";
  auto collection = device::mojom::HidCollectionInfo::New();
  collection->usage = device::mojom::HidUsageAndPage::New(1, 1);
  collection->input_reports.push_back(
      device::mojom::HidReportDescription::New());
  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, 2);
  collection->output_reports.push_back(
      device::mojom::HidReportDescription::New());
  device_info->collections.push_back(std::move(collection));
  return device_info;
}

}  // namespace

TEST_F(HidChooserControllerTest, DeviceChangeUpdatesDeviceInfo) {
  const char kTestGuid[] = "guid";

  // Connect a partially-initialized device with one collection.
  base::RunLoop device_added_loop;
  EXPECT_CALL(device_observer(), OnDeviceAdded).WillOnce([&](const auto& d) {
    EXPECT_EQ(d.guid, kTestGuid);
    EXPECT_EQ(d.collections.size(), 1u);
    device_added_loop.Quit();
  });
  auto partial_device = CreateDeviceWithOneCollection(kTestGuid);
  ConnectDevice(*partial_device);
  device_added_loop.Run();

  // Create the HidChooserController.
  base::MockCallback<content::HidChooser::Callback> callback;
  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized)
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));
  auto hid_chooser_controller =
      CreateHidChooserController({}, {}, callback.Get());
  options_initialized_loop.Run();

  // Check that the option is present.
  EXPECT_EQ(hid_chooser_controller->NumOptions(), 1u);
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));

  // Update the device to add another collection.
  base::RunLoop device_changed_loop;
  EXPECT_CALL(device_observer(), OnDeviceChanged).WillOnce([&](const auto& d) {
    EXPECT_EQ(d.guid, kTestGuid);
    EXPECT_EQ(d.collections.size(), 2u);
    device_changed_loop.Quit();
  });
  auto complete_device = CreateDeviceWithTwoCollections(kTestGuid);
  UpdateDevice(*complete_device);
  device_changed_loop.Run();

  // Check that the option is still present and the name has not changed.
  EXPECT_EQ(hid_chooser_controller->NumOptions(), 1u);
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));

  // Select the option and check that only the updated device info is returned.
  base::RunLoop callback_loop;
  EXPECT_CALL(callback, Run).WillOnce([&](auto devices) {
    ASSERT_EQ(devices.size(), 1u);
    EXPECT_EQ(devices[0]->guid, kTestGuid);
    EXPECT_EQ(devices[0]->collections.size(), 2u);
    callback_loop.Quit();
  });
  hid_chooser_controller->Select({0});
  callback_loop.Run();
}

TEST_F(HidChooserControllerTest, ExcludedDeviceChangedToIncludedDevice) {
  const char kTestGuid[] = "guid";

  // Connect a partially-initialized device with one collection.
  base::RunLoop device_added_loop;
  EXPECT_CALL(device_observer(), OnDeviceAdded).WillOnce([&](const auto& d) {
    EXPECT_EQ(d.guid, kTestGuid);
    EXPECT_EQ(d.collections.size(), 1u);
    device_added_loop.Quit();
  });
  auto partial_device = CreateDeviceWithOneCollection(kTestGuid);
  ConnectDevice(*partial_device);
  device_added_loop.Run();

  // Create the HidChooserController. Set a usage page filter that excludes
  // |partial_device|.
  std::vector<blink::mojom::HidDeviceFilterPtr> filters;
  filters.push_back(
      blink::mojom::HidDeviceFilter::New(nullptr, CreatePageFilter(2)));
  base::MockCallback<content::HidChooser::Callback> callback;
  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized)
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));
  auto hid_chooser_controller =
      CreateHidChooserController(std::move(filters), {}, callback.Get());
  options_initialized_loop.Run();

  // Check that the option is not present.
  EXPECT_EQ(hid_chooser_controller->NumOptions(), 0u);

  // Update the device to add another collection. Now the device should be
  // allowed by the filter.
  base::RunLoop option_added_loop;
  EXPECT_CALL(view(), OnOptionAdded(0))
      .WillOnce(RunClosure(option_added_loop.QuitClosure()));
  EXPECT_CALL(device_observer(), OnDeviceChanged).WillOnce([&](const auto& d) {
    EXPECT_EQ(d.guid, kTestGuid);
    EXPECT_EQ(d.collections.size(), 2u);
  });
  auto complete_device = CreateDeviceWithTwoCollections(kTestGuid);
  UpdateDevice(*complete_device);
  option_added_loop.Run();

  // Check that the option has been added.
  EXPECT_EQ(hid_chooser_controller->NumOptions(), 1u);
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));

  // Select the option and check that only the updated device info is returned.
  base::RunLoop callback_loop;
  EXPECT_CALL(callback, Run).WillOnce([&](auto devices) {
    ASSERT_EQ(devices.size(), 1u);
    EXPECT_EQ(devices[0]->guid, kTestGuid);
    EXPECT_EQ(devices[0]->collections.size(), 2u);
    callback_loop.Quit();
  });
  hid_chooser_controller->Select({0});
  callback_loop.Run();
}

TEST_F(HidChooserControllerTest, DeviceChangedToBlockedDevice) {
  const char kTestGuid[] = "guid";

  // Connect a partially-initialized device with one collection.
  base::RunLoop device_added_loop;
  EXPECT_CALL(device_observer(), OnDeviceAdded).WillOnce([&](const auto& d) {
    EXPECT_EQ(d.guid, kTestGuid);
    EXPECT_EQ(d.collections.size(), 1u);
    device_added_loop.Quit();
  });
  auto partial_device = CreateDeviceWithOneCollection(kTestGuid);
  ConnectDevice(*partial_device);
  device_added_loop.Run();

  // Create the HidChooserController.
  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized)
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));
  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  // Check that the option is present.
  EXPECT_EQ(hid_chooser_controller->NumOptions(), 1u);
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));

  // Update the device to add another collection with the FIDO usage page.
  // Devices with any FIDO collection are blocked, so the chooser option should
  // be removed.
  base::RunLoop option_removed_loop;
  EXPECT_CALL(view(), OnOptionRemoved(0))
      .WillOnce(RunClosure(option_removed_loop.QuitClosure()));
  EXPECT_CALL(device_observer(), OnDeviceChanged).WillOnce([&](const auto& d) {
    EXPECT_EQ(d.guid, kTestGuid);
    EXPECT_EQ(d.collections.size(), 2u);
  });
  auto complete_device = CreateDeviceWithTwoCollections(kTestGuid);
  complete_device->collections[1]->usage->usage_page = device::mojom::kPageFido;
  UpdateDevice(*complete_device);
  option_removed_loop.Run();

  // Check that the option has been removed.
  EXPECT_EQ(hid_chooser_controller->NumOptions(), 0u);
}

class HidChooserControllerFidoTest : public HidChooserControllerTest,
                                     public testing::WithParamInterface<bool> {
};

TEST_P(HidChooserControllerFidoTest, FidoDeviceShownWithPrivilegedOrigin) {
  const bool is_fido_allowed = GetParam();
  if (is_fido_allowed) {
    // Use the gnubbyd-v3 dev key for testing since it is already on the
    // allowlist.
    OverrideLastCommittedOrigin(
        main_rfh(),
        url::Origin::Create(
            GURL("chrome-extension://ckcendljdlmgnhghiaomidhiiclmapok/")));
  }

  // Connect a FIDO device.
  TestFuture<device::mojom::HidDeviceInfoPtr> connected_device;
  EXPECT_CALL(device_observer(), OnDeviceAdded).WillOnce([&](const auto& d) {
    connected_device.SetValue(d.Clone());
  });
  auto device = device::CreateDeviceFromReportDescriptor(
      /*vendor_id=*/0x1234,
      /*product_id=*/0xabcd, device::TestReportDescriptors::FidoU2fHid());
  ConnectDevice(*device);
  ASSERT_EQ(1u, connected_device.Get()->collections.size());
  ASSERT_TRUE(connected_device.Get()->collections[0]->usage);
  EXPECT_EQ(device::mojom::kPageFido,
            connected_device.Get()->collections[0]->usage->usage_page);

  // Create the HidChooserController.
  TestFuture<bool> options_initialized;
  EXPECT_CALL(view(), OnOptionsInitialized).WillOnce([&]() {
    options_initialized.SetValue(true);
  });
  TestFuture<std::vector<device::mojom::HidDeviceInfoPtr>> selected_devices;
  auto hid_chooser_controller =
      CreateHidChooserController({}, {}, selected_devices.GetCallback());
  EXPECT_TRUE(options_initialized.Get());

  // If the origin is not allowed to access FIDO reports there should be no
  // devices in the chooser.
  if (!is_fido_allowed) {
    EXPECT_EQ(0u, hid_chooser_controller->NumOptions());
    return;
  }

  // If the origin is allowed to access FIDO reports there should be one option.
  ASSERT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"Test Device", hid_chooser_controller->GetOption(0));

  // Select the option and check that the returned device contains FIDO reports.
  hid_chooser_controller->Select({0});
  ASSERT_EQ(1u, selected_devices.Get().size());
  EXPECT_EQ(device->guid, selected_devices.Get()[0]->guid);
  ASSERT_EQ(1u, selected_devices.Get()[0]->collections.size());
  const auto& collection = selected_devices.Get()[0]->collections[0];
  ASSERT_TRUE(collection->usage);
  EXPECT_EQ(device::mojom::kPageFido, collection->usage->usage_page);
  EXPECT_EQ(1u, collection->input_reports.size());
  EXPECT_EQ(1u, collection->output_reports.size());
}

TEST_F(HidChooserControllerTest, FlakyDeviceName) {
  base::RunLoop device_added_loop1;
  base::RunLoop device_added_loop2;
  base::RunLoop device_added_loop3;
  EXPECT_CALL(device_observer(), OnDeviceAdded(_))
      .WillOnce(RunClosure(device_added_loop1.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop2.QuitClosure()))
      .WillOnce(RunClosure(device_added_loop3.QuitClosure()));

  base::RunLoop options_initialized_loop;
  EXPECT_CALL(view(), OnOptionsInitialized())
      .WillOnce(RunClosure(options_initialized_loop.QuitClosure()));

  // Connect a device with an empty product name string.
  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "", "001");
  device_added_loop1.Run();

  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "a", "001");
  device_added_loop2.Run();

  CreateAndAddFakeHidDevice(kTestPhysicalDeviceIds[0], 1, 1, "", "001");
  device_added_loop3.Run();

  auto hid_chooser_controller = CreateHidChooserController({});
  options_initialized_loop.Run();

  EXPECT_EQ(1u, hid_chooser_controller->NumOptions());
  EXPECT_EQ(u"a", hid_chooser_controller->GetOption(0));
}

INSTANTIATE_TEST_SUITE_P(HidChooserControllerFidoTests,
                         HidChooserControllerFidoTest,
                         testing::Values(false, true));
