// 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 "base/strings/stringprintf.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/values_test_util.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/profiles/profile.h"
#include "components/content_settings/core/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/api/usb/usb_api.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "services/device/public/cpp/test/fake_usb_device_info.h"
#include "services/device/public/cpp/test/fake_usb_device_manager.h"
#include "services/device/public/cpp/test/mock_usb_mojo_device.h"
#include "services/device/public/mojom/usb_device.mojom.h"

namespace extensions {
namespace {

using ::base::test::RunOnceCallback;
using ::testing::_;

constexpr char kManifest[] =
    R"(
      {
        "name": "ChromeUsbApiTest App",
        "version": "1.0",
        "manifest_version": 2,
        "app": {
          "background": {
            "scripts": ["background_script.js"]
          }
        },
        "permissions": ["usb"]
      }
    )";

// Need to fill it with an extension's url.
constexpr char kPolicySetting[] = R"(
    [
      {
        "devices": [{ "vendor_id": 0, "product_id": 0 }],
        "urls": ["%s"]
      }
    ])";

class ChromeUsbApiTest : public ExtensionBrowserTest {
 public:
  void SetUpOnMainThread() override {
    ExtensionBrowserTest::SetUpOnMainThread();

    // Set fake USB device manager for extensions::UsbDeviceManager.
    mojo::PendingRemote<device::mojom::UsbDeviceManager> usb_manager;
    fake_usb_manager_.AddReceiver(usb_manager.InitWithNewPipeAndPassReceiver());
    UsbDeviceManager::Get(profile())->SetDeviceManagerForTesting(
        std::move(usb_manager));
    base::RunLoop().RunUntilIdle();
  }

 protected:
  void AddFakeDevice() {
    std::vector<device::mojom::UsbConfigurationInfoPtr> configs;
    configs.push_back(
        device::FakeUsbDeviceInfo::CreateConfiguration(0xff, 0x00, 0x00, 1));
    configs.push_back(
        device::FakeUsbDeviceInfo::CreateConfiguration(0xff, 0x00, 0x00, 2));

    fake_device_ = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
        0, 0, "Test Manufacturer", "Test Device", "ABC123", std::move(configs));
    fake_usb_manager_.AddDevice(fake_device_);
    fake_usb_manager_.SetMockForDevice(fake_device_->guid(), &mock_device_);
    base::RunLoop().RunUntilIdle();
  }

  void SetUpPolicy(const Extension* extension) {
    profile()->GetPrefs()->Set(
        prefs::kManagedWebUsbAllowDevicesForUrls,
        base::test::ParseJson(base::StringPrintf(
            kPolicySetting, extension->url().spec().c_str())));
  }

  // `mock_device_`, `fake_device_`, and `fake_usb_manager_` must be declared in
  // this order to avoid dangling pointers.
  device::MockUsbMojoDevice mock_device_;
  scoped_refptr<device::FakeUsbDeviceInfo> fake_device_;
  device::FakeUsbDeviceManager fake_usb_manager_;
};

IN_PROC_BROWSER_TEST_F(ChromeUsbApiTest, GetDevicesByPolicy) {
  extensions::TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), R"(
    chrome.test.sendMessage("ready", async () => {
      chrome.usb.getDevices({
        vendorId: 0,
        productId: 0
      }, function(devices) {
        chrome.test.assertEq(1, devices.length);
        const device = devices[0];
        chrome.test.assertEq(0, device.vendorId);
        chrome.test.assertEq(0, device.productId);
        chrome.test.assertEq(0x0100, device.version);
        chrome.test.assertEq("Test Device", device.productName);
        chrome.test.assertEq("Test Manufacturer", device.manufacturerName);
        chrome.test.assertEq("ABC123", device.serialNumber);
        chrome.test.notifyPass();
      });
    });
  )");

  // Launch the test app.
  ExtensionTestMessageListener ready_listener("ready",
                                              ReplyBehavior::kWillReply);
  extensions::ResultCatcher result_catcher;
  const Extension* extension = LoadExtension(dir.UnpackedPath());

  // Run the test.
  SetUpPolicy(extension);
  AddFakeDevice();
  EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
  ready_listener.Reply("ok");
  EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

IN_PROC_BROWSER_TEST_F(ChromeUsbApiTest, GetDevicesByPolicyNoPolicySet) {
  extensions::TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), R"(
    chrome.test.sendMessage("ready", async () => {
      chrome.usb.getDevices({
        vendorId: 0,
        productId: 0
      }, function(devices) {
        chrome.test.assertEq(0, devices.length);
        chrome.test.notifyPass();
      });
    });
  )");

  // Launch the test app.
  ExtensionTestMessageListener ready_listener("ready",
                                              ReplyBehavior::kWillReply);
  extensions::ResultCatcher result_catcher;
  LoadExtension(dir.UnpackedPath());

  // Run the test.
  AddFakeDevice();
  EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
  ready_listener.Reply("ok");
  EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

IN_PROC_BROWSER_TEST_F(ChromeUsbApiTest, FindDevicesByPolicy) {
  extensions::TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), R"(
    chrome.test.sendMessage("ready", async () => {
      chrome.usb.findDevices({
        vendorId: 0,
        productId: 0
      }, function(devices) {
        chrome.test.assertEq(1, devices.length);
        const device = devices[0];
        chrome.test.assertEq(0, device.vendorId);
        chrome.test.assertEq(0, device.productId);
        chrome.test.notifyPass();
      });
    });
  )");

  // Launch the test app.
  ExtensionTestMessageListener ready_listener("ready",
                                              ReplyBehavior::kWillReply);
  extensions::ResultCatcher result_catcher;
  const Extension* extension = LoadExtension(dir.UnpackedPath());

  EXPECT_CALL(mock_device_, Open)
      .WillOnce(
          RunOnceCallback<0>(device::mojom::UsbOpenDeviceResult::NewSuccess(
              device::mojom::UsbOpenDeviceSuccess::OK)));

  // Run the test.
  SetUpPolicy(extension);
  AddFakeDevice();
  EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
  ready_listener.Reply("ok");
  EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

IN_PROC_BROWSER_TEST_F(ChromeUsbApiTest, FindDevicesByPolicyNoPolicySet) {
  extensions::TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), R"(
    chrome.test.sendMessage("ready", async () => {
      chrome.usb.findDevices({
        vendorId: 0,
        productId: 0
      }, function(devices) {
        chrome.test.assertEq(undefined, devices);
        chrome.test.notifyPass();
      });
    });
  )");

  // Launch the test app.
  ExtensionTestMessageListener ready_listener("ready",
                                              ReplyBehavior::kWillReply);
  extensions::ResultCatcher result_catcher;
  LoadExtension(dir.UnpackedPath());

  // Run the test.
  AddFakeDevice();
  EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
  ready_listener.Reply("ok");
  EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

IN_PROC_BROWSER_TEST_F(ChromeUsbApiTest, OnDevicesAdded) {
  extensions::TestExtensionDir dir;
  dir.WriteManifest(kManifest);
  dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), R"(
    chrome.usb.onDeviceAdded.addListener(function(device) {
      chrome.test.assertEq(0, device.vendorId);
      chrome.test.assertEq(0, device.productId);
      chrome.test.assertEq(0x0100, device.version);
      chrome.test.assertEq("Test Device", device.productName);
      chrome.test.assertEq("Test Manufacturer", device.manufacturerName);
      chrome.test.assertEq("ABC123", device.serialNumber);
      chrome.test.notifyPass();
    });
  )");

  // Launch the test app.
  extensions::ResultCatcher result_catcher;
  const Extension* extension = LoadExtension(dir.UnpackedPath());

  // Run the test.
  SetUpPolicy(extension);
  AddFakeDevice();
  EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}

}  // namespace
}  // namespace extensions
