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

#include "device/fido/fido_cable_discovery.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "base/containers/span.h"
#include "base/stl_util.h"
#include "build/build_config.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "device/bluetooth/bluetooth_advertisement.h"
#include "device/bluetooth/test/bluetooth_test.h"
#include "device/bluetooth/test/mock_bluetooth_adapter.h"
#include "device/fido/fido_ble_device.h"
#include "device/fido/fido_ble_uuids.h"
#include "device/fido/fido_cable_handshake_handler.h"
#include "device/fido/fido_parsing_utils.h"
#include "device/fido/mock_fido_discovery_observer.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::_;
using ::testing::NiceMock;

namespace device {

namespace {

constexpr uint8_t kTestCableVersionNumber = 0x01;

// Constants required for discovering and constructing a Cable device that
// are given by the relying party via an extension.
constexpr FidoCableDiscovery::EidArray kClientEid = {
    {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11,
     0x12, 0x13, 0x14, 0x15}};

constexpr char kUuidFormattedClientEid[] =
    "00010203-0405-0607-0809-101112131415";

constexpr FidoCableDiscovery::EidArray kAuthenticatorEid = {
    {0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
     0x01, 0x01, 0x01, 0x01}};

constexpr FidoCableDiscovery::EidArray kInvalidAuthenticatorEid = {
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00}};

constexpr FidoCableDiscovery::SessionPreKeyArray kTestSessionPreKey = {
    {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}};

constexpr FidoCableDiscovery::EidArray kSecondaryClientEid = {
    {0x15, 0x14, 0x13, 0x12, 0x11, 0x10, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04,
     0x03, 0x02, 0x01, 0x00}};

constexpr char kUuidFormattedSecondaryClientEid[] =
    "15141312-1110-0908-0706-050403020100";

constexpr FidoCableDiscovery::EidArray kSecondaryAuthenticatorEid = {
    {0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee,
     0xee, 0xee, 0xee, 0xee}};

constexpr FidoCableDiscovery::SessionPreKeyArray kSecondarySessionPreKey = {
    {0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
     0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd,
     0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd}};

// Below constants are used to construct MockBluetoothDevice for testing.
constexpr char kTestBleDeviceAddress[] = "11:12:13:14:15:16";

constexpr char kTestBleDeviceName[] = "test_cable_device";

std::unique_ptr<MockBluetoothDevice> CreateTestBluetoothDevice() {
  return std::make_unique<testing::NiceMock<MockBluetoothDevice>>(
      nullptr /* adapter */, 0 /* bluetooth_class */, kTestBleDeviceName,
      kTestBleDeviceAddress, true /* paired */, true /* connected */);
}

// Matcher to compare the content of advertisement data received from the
// client.
MATCHER_P2(IsAdvertisementContent,
           expected_client_eid,
           expected_uuid_formatted_client_eid,
           "") {
#if defined(OS_MACOSX)
  const auto uuid_list = arg->service_uuids();
  return std::any_of(uuid_list->begin(), uuid_list->end(),
                     [this](const auto& uuid) {
                       return uuid == expected_uuid_formatted_client_eid;
                     });

#elif defined(OS_WIN)
  const auto manufacturer_data = arg->manufacturer_data();
  const auto manufacturer_data_value = manufacturer_data->find(0xFFFD);

  if (manufacturer_data_value == manufacturer_data->end())
    return false;

  return fido_parsing_utils::ExtractSuffixSpan(manufacturer_data_value->second,
                                               2) == expected_client_eid;

#elif defined(OS_LINUX) || defined(OS_CHROMEOS)
  const auto service_data = arg->service_data();
  const auto service_data_with_uuid =
      service_data->find(kCableAdvertisementUUID);

  if (service_data_with_uuid == service_data->end())
    return false;

  const auto& service_data_value = service_data_with_uuid->second;
  return (service_data_value[0] >> 5 & 1) &&
         service_data_value[1] == kTestCableVersionNumber &&
         service_data_value.size() == 18u &&
         base::make_span(service_data_value).subspan(2) == expected_client_eid;

#endif

  return true;
}

class CableMockBluetoothAdvertisement : public BluetoothAdvertisement {
 public:
  MOCK_METHOD2(Unregister,
               void(const SuccessCallback& success_callback,
                    const ErrorCallback& error_callback));

 private:
  ~CableMockBluetoothAdvertisement() override = default;
};

// Mock BLE adapter that abstracts out authenticator logic with the following
// logic:
//  - Responds to BluetoothAdapter::RegisterAdvertisement() by always invoking
//    success callback.
//  - Responds to BluetoothAdapter::StartDiscoverySessionWithFilter() by
//    invoking BluetoothAdapter::Observer::DeviceAdded() on a test bluetooth
//    device that includes service data containing authenticator EID.
class CableMockAdapter : public MockBluetoothAdapter {
 public:
  MOCK_METHOD3(RegisterAdvertisement,
               void(std::unique_ptr<BluetoothAdvertisement::Data>,
                    const CreateAdvertisementCallback&,
                    const AdvertisementErrorCallback&));

  void AddNewTestBluetoothDevice(
      base::span<const uint8_t, FidoCableDiscovery::kEphemeralIdSize>
          authenticator_eid) {
    auto mock_device = CreateTestBluetoothDevice();

    std::vector<uint8_t> service_data(18);
    service_data[0] = 1 << 5;
    std::copy(authenticator_eid.begin(), authenticator_eid.end(),
              service_data.begin() + 2);
    BluetoothDevice::ServiceDataMap service_data_map;
    service_data_map.emplace(kCableAdvertisementUUID, std::move(service_data));

    mock_device->UpdateAdvertisementData(
        1 /* rssi */, base::nullopt /* flags */, BluetoothDevice::UUIDList(),
        base::nullopt /* tx_power */, std::move(service_data_map),
        BluetoothDevice::ManufacturerDataMap());

    auto* mock_device_ptr = mock_device.get();
    AddMockDevice(std::move(mock_device));

    for (auto& observer : GetObservers())
      observer.DeviceAdded(this, mock_device_ptr);
  }

  void ExpectRegisterAdvertisementWithResponse(
      bool simulate_success,
      base::span<const uint8_t> expected_client_eid,
      base::StringPiece expected_uuid_formatted_client_eid,
      scoped_refptr<CableMockBluetoothAdvertisement> advertisement_ptr =
          nullptr) {
    if (!advertisement_ptr)
      advertisement_ptr =
          base::MakeRefCounted<CableMockBluetoothAdvertisement>();

    EXPECT_CALL(*this,
                RegisterAdvertisement(
                    IsAdvertisementContent(expected_client_eid,
                                           expected_uuid_formatted_client_eid),
                    _, _))
        .WillOnce(::testing::WithArgs<1, 2>(
            [simulate_success, advertisement_ptr](
                const auto& success_callback, const auto& failure_callback) {
              simulate_success
                  ? success_callback.Run(advertisement_ptr)
                  : failure_callback.Run(BluetoothAdvertisement::ErrorCode::
                                             INVALID_ADVERTISEMENT_ERROR_CODE);
            }));
  }

  void ExpectSuccessCallbackToSetPowered() {
    EXPECT_CALL(*this, SetPowered(true, _, _))
        .WillOnce(::testing::WithArg<1>(
            [](const auto& callback) { callback.Run(); }));
  }

 protected:
  ~CableMockAdapter() override = default;
};

class FakeHandshakeHandler : public FidoCableHandshakeHandler {
 public:
  FakeHandshakeHandler(FidoCableDevice* device,
                       base::span<const uint8_t, 8> nonce,
                       base::span<const uint8_t, 32> session_pre_key)
      : FidoCableHandshakeHandler(device, nonce, session_pre_key) {}
  ~FakeHandshakeHandler() override = default;

  void InitiateCableHandshake(FidoDevice::DeviceCallback callback) override {
    std::move(callback).Run(std::vector<uint8_t>());
  }

  bool ValidateAuthenticatorHandshakeMessage(
      base::span<const uint8_t> response) override {
    return true;
  }
};

// Fake discovery that encapsulates exactly the same behavior as
// FidoCableDiscovery except that it uses FakeHandshakeHandler instead of
// FidoHandshakeHandler to conduct handshake with the authenticator.
class FakeFidoCableDiscovery : public FidoCableDiscovery {
 public:
  explicit FakeFidoCableDiscovery(
      std::vector<CableDiscoveryData> discovery_data)
      : FidoCableDiscovery(std::move(discovery_data)) {}
  ~FakeFidoCableDiscovery() override = default;

 private:
  std::unique_ptr<FidoCableHandshakeHandler> CreateHandshakeHandler(
      FidoCableDevice* device,
      base::span<const uint8_t, kSessionPreKeySize> session_pre_key,
      base::span<const uint8_t, 8> nonce) override {
    return std::make_unique<FakeHandshakeHandler>(device, nonce,
                                                  session_pre_key);
  }
};

}  // namespace

class FidoCableDiscoveryTest : public ::testing::Test {
 public:
  std::unique_ptr<FidoCableDiscovery> CreateDiscovery() {
    std::vector<FidoCableDiscovery::CableDiscoveryData> discovery_data;
    discovery_data.emplace_back(kTestCableVersionNumber, kClientEid,
                                kAuthenticatorEid, kTestSessionPreKey);
    return std::make_unique<FakeFidoCableDiscovery>(std::move(discovery_data));
  }

  base::test::ScopedTaskEnvironment scoped_task_environment_;
};

// Tests regular successful discovery flow for Cable device.
TEST_F(FidoCableDiscoveryTest, TestDiscoveryFindsNewDevice) {
  auto cable_discovery = CreateDiscovery();
  NiceMock<MockFidoDiscoveryObserver> mock_observer;
  EXPECT_CALL(mock_observer, DeviceAdded(_, _));
  cable_discovery->set_observer(&mock_observer);

  auto mock_adapter =
      base::MakeRefCounted<::testing::NiceMock<CableMockAdapter>>();
  ::testing::InSequence testing_sequence;
  mock_adapter->ExpectSuccessCallbackToSetPowered();
  mock_adapter->ExpectRegisterAdvertisementWithResponse(
      true /* simulate_success */, kClientEid, kUuidFormattedClientEid);
  EXPECT_CALL(*mock_adapter, StartDiscoverySessionWithFilterRaw(_, _, _))
      .WillOnce(::testing::WithArg<1>([&mock_adapter](const auto& callback) {
        mock_adapter->AddNewTestBluetoothDevice(kAuthenticatorEid);
        callback.Run(nullptr);
      }));

  BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter);
  cable_discovery->Start();
  scoped_task_environment_.RunUntilIdle();
}

// Tests a scenario where upon broadcasting advertisement and scanning, client
// discovers a device with an incorrect authenticator EID. Observer::AddDevice()
// must not be called.
TEST_F(FidoCableDiscoveryTest, TestDiscoveryFindsIncorrectDevice) {
  auto cable_discovery = CreateDiscovery();
  NiceMock<MockFidoDiscoveryObserver> mock_observer;
  EXPECT_CALL(mock_observer, DeviceAdded(_, _)).Times(0);
  cable_discovery->set_observer(&mock_observer);

  auto mock_adapter =
      base::MakeRefCounted<::testing::NiceMock<CableMockAdapter>>();
  ::testing::InSequence testing_sequence;
  mock_adapter->ExpectSuccessCallbackToSetPowered();
  mock_adapter->ExpectRegisterAdvertisementWithResponse(
      true /* simulate_success */, kClientEid, kUuidFormattedClientEid);
  EXPECT_CALL(*mock_adapter, StartDiscoverySessionWithFilterRaw(_, _, _))
      .WillOnce(::testing::WithArg<1>([&mock_adapter](const auto& callback) {
        mock_adapter->AddNewTestBluetoothDevice(kInvalidAuthenticatorEid);
        callback.Run(nullptr);
      }));

  BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter);
  cable_discovery->Start();
  scoped_task_environment_.RunUntilIdle();
}

// Tests Cable discovery flow when multiple(2) sets of client/authenticator EIDs
// are passed on from the relying party. We should expect 2 invocations of
// BluetoothAdapter::RegisterAdvertisement().
TEST_F(FidoCableDiscoveryTest, TestDiscoveryWithMultipleEids) {
  std::vector<FidoCableDiscovery::CableDiscoveryData> discovery_data;
  discovery_data.emplace_back(kTestCableVersionNumber, kClientEid,
                              kAuthenticatorEid, kTestSessionPreKey);
  discovery_data.emplace_back(kTestCableVersionNumber, kSecondaryClientEid,
                              kSecondaryAuthenticatorEid,
                              kSecondarySessionPreKey);
  auto cable_discovery =
      std::make_unique<FakeFidoCableDiscovery>(std::move(discovery_data));
  NiceMock<MockFidoDiscoveryObserver> mock_observer;
  EXPECT_CALL(mock_observer, DeviceAdded(_, _));
  cable_discovery->set_observer(&mock_observer);

  auto mock_adapter =
      base::MakeRefCounted<::testing::NiceMock<CableMockAdapter>>();
  ::testing::InSequence testing_sequence;
  mock_adapter->ExpectSuccessCallbackToSetPowered();
  mock_adapter->ExpectRegisterAdvertisementWithResponse(
      true /* simulate_success */, kClientEid, kUuidFormattedClientEid);
  mock_adapter->ExpectRegisterAdvertisementWithResponse(
      true /* simulate_success */, kSecondaryClientEid,
      kUuidFormattedSecondaryClientEid);
  EXPECT_CALL(*mock_adapter, StartDiscoverySessionWithFilterRaw(_, _, _))
      .WillOnce(::testing::WithArg<1>([&mock_adapter](const auto& callback) {
        mock_adapter->AddNewTestBluetoothDevice(kAuthenticatorEid);
        callback.Run(nullptr);
      }));

  BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter);
  cable_discovery->Start();
  scoped_task_environment_.RunUntilIdle();
}

// Tests a scenario where only one of the two client EID's are advertised
// successfully. Since at least one advertisement are successfully processed,
// scanning process should be invoked.
TEST_F(FidoCableDiscoveryTest, TestDiscoveryWithPartialAdvertisementSuccess) {
  std::vector<FidoCableDiscovery::CableDiscoveryData> discovery_data;
  discovery_data.emplace_back(kTestCableVersionNumber, kClientEid,
                              kAuthenticatorEid, kTestSessionPreKey);
  discovery_data.emplace_back(kTestCableVersionNumber, kSecondaryClientEid,
                              kSecondaryAuthenticatorEid,
                              kSecondarySessionPreKey);
  auto cable_discovery =
      std::make_unique<FakeFidoCableDiscovery>(std::move(discovery_data));
  NiceMock<MockFidoDiscoveryObserver> mock_observer;
  EXPECT_CALL(mock_observer, DeviceAdded(_, _));
  cable_discovery->set_observer(&mock_observer);

  auto mock_adapter =
      base::MakeRefCounted<::testing::NiceMock<CableMockAdapter>>();
  ::testing::InSequence testing_sequence;
  mock_adapter->ExpectSuccessCallbackToSetPowered();
  mock_adapter->ExpectRegisterAdvertisementWithResponse(
      true /* simulate_success */, kClientEid, kUuidFormattedClientEid);
  mock_adapter->ExpectRegisterAdvertisementWithResponse(
      false /* simulate_success */, kSecondaryClientEid,
      kUuidFormattedSecondaryClientEid);
  EXPECT_CALL(*mock_adapter, StartDiscoverySessionWithFilterRaw(_, _, _))
      .WillOnce(::testing::WithArg<1>([&mock_adapter](const auto& callback) {
        mock_adapter->AddNewTestBluetoothDevice(kAuthenticatorEid);
        callback.Run(nullptr);
      }));

  BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter);
  cable_discovery->Start();
  scoped_task_environment_.RunUntilIdle();
}

// Test the scenario when all advertisement for client EID's fails.
TEST_F(FidoCableDiscoveryTest, TestDiscoveryWithAdvertisementFailures) {
  std::vector<FidoCableDiscovery::CableDiscoveryData> discovery_data;
  discovery_data.emplace_back(kTestCableVersionNumber, kClientEid,
                              kAuthenticatorEid, kTestSessionPreKey);
  discovery_data.emplace_back(kTestCableVersionNumber, kSecondaryClientEid,
                              kSecondaryAuthenticatorEid,
                              kSecondarySessionPreKey);
  auto cable_discovery =
      std::make_unique<FakeFidoCableDiscovery>(std::move(discovery_data));

  NiceMock<MockFidoDiscoveryObserver> mock_observer;
  EXPECT_CALL(mock_observer, DeviceAdded(_, _)).Times(0);
  cable_discovery->set_observer(&mock_observer);

  auto mock_adapter =
      base::MakeRefCounted<::testing::NiceMock<CableMockAdapter>>();
  ::testing::InSequence testing_sequence;
  mock_adapter->ExpectSuccessCallbackToSetPowered();
  mock_adapter->ExpectRegisterAdvertisementWithResponse(
      false /* simulate_success */, kClientEid, kUuidFormattedClientEid);
  mock_adapter->ExpectRegisterAdvertisementWithResponse(
      false /* simulate_success */, kSecondaryClientEid,
      kUuidFormattedSecondaryClientEid);
  EXPECT_CALL(*mock_adapter, StartDiscoverySessionWithFilterRaw(_, _, _))
      .Times(0);

  BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter);
  cable_discovery->Start();
  scoped_task_environment_.RunUntilIdle();
}

TEST_F(FidoCableDiscoveryTest, TestUnregisterAdvertisementUponDestruction) {
  auto cable_discovery = CreateDiscovery();
  CableMockBluetoothAdvertisement* advertisement =
      new CableMockBluetoothAdvertisement();
  EXPECT_CALL(*advertisement, Unregister(_, _)).Times(1);

  ::testing::InSequence testing_sequence;
  auto mock_adapter =
      base::MakeRefCounted<::testing::NiceMock<CableMockAdapter>>();
  mock_adapter->ExpectSuccessCallbackToSetPowered();
  mock_adapter->ExpectRegisterAdvertisementWithResponse(
      true /* simulate_success */, kClientEid, kUuidFormattedClientEid,
      base::WrapRefCounted(advertisement));

  BluetoothAdapterFactory::SetAdapterForTesting(mock_adapter);
  cable_discovery->Start();
  scoped_task_environment_.RunUntilIdle();

  EXPECT_EQ(1u, cable_discovery->advertisements_.size());
  cable_discovery.reset();
}

}  // namespace device
