chromium/chromeos/ash/services/multidevice_setup/host_backend_delegate_impl_unittest.cc

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

#include "chromeos/ash/services/multidevice_setup/host_backend_delegate_impl.h"

#include <memory>
#include <optional>

#include "ash/constants/ash_features.h"
#include "base/containers/flat_map.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "base/timer/mock_timer.h"
#include "base/unguessable_token.h"
#include "chromeos/ash/components/multidevice/remote_device_test_util.h"
#include "chromeos/ash/components/multidevice/software_feature.h"
#include "chromeos/ash/components/multidevice/software_feature_state.h"
#include "chromeos/ash/services/device_sync/public/cpp/fake_device_sync_client.h"
#include "chromeos/ash/services/multidevice_setup/fake_eligible_host_devices_provider.h"
#include "chromeos/ash/services/multidevice_setup/fake_host_backend_delegate.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

namespace multidevice_setup {

namespace {

const char kPendingRequestHostIdPrefName[] =
    "multidevice_setup.pending_request_host_id";
const char kPendingRemovalOfCurrentHost[] = "pendingRemovalOfCurrentHost";
const char kNoPendingRequest[] = "";

const size_t kNumTestDevices = 4;

}  // namespace

class MultiDeviceSetupHostBackendDelegateImplTest
    : public ::testing::TestWithParam<bool> {
 public:
  MultiDeviceSetupHostBackendDelegateImplTest(
      const MultiDeviceSetupHostBackendDelegateImplTest&) = delete;
  MultiDeviceSetupHostBackendDelegateImplTest& operator=(
      const MultiDeviceSetupHostBackendDelegateImplTest&) = delete;

 protected:
  MultiDeviceSetupHostBackendDelegateImplTest()
      : test_devices_(
            multidevice::CreateRemoteDeviceRefListForTest(kNumTestDevices)) {}
  ~MultiDeviceSetupHostBackendDelegateImplTest() override = default;

  // testing::Test:
  void SetUp() override {
    SetFeatureFlags(GetParam() /* use_v1_devicesync */);

    // Tests are run once to simulate when v1 DeviceSync is enabled and once to
    // simulate when it is disabled, leaving only v2 DeviceSync operational. In
    // the former case, only public keys are needed, and in the latter case,
    // only Instance IDs are needed.
    for (multidevice::RemoteDeviceRef device : test_devices_) {
      if (features::ShouldUseV1DeviceSync())
        GetMutableRemoteDevice(device)->instance_id.clear();
      else
        GetMutableRemoteDevice(device)->public_key.clear();
    }

    fake_eligible_host_devices_provider_ =
        std::make_unique<FakeEligibleHostDevicesProvider>();
    fake_eligible_host_devices_provider_->set_eligible_host_devices(
        test_devices_);

    test_pref_service_ =
        std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
    HostBackendDelegateImpl::RegisterPrefs(test_pref_service_->registry());

    fake_device_sync_client_ =
        std::make_unique<device_sync::FakeDeviceSyncClient>();
    fake_device_sync_client_->set_synced_devices(test_devices_);
  }

  void TearDown() override {
    if (delegate_)
      delegate_->RemoveObserver(observer_.get());
  }

  void CreateDelegate(
      const std::optional<multidevice::RemoteDeviceRef>& initial_host,
      const std::string& initial_pending_host_request = kNoPendingRequest) {
    SetHostInDeviceSyncClient(initial_host);
    test_pref_service_->SetString(kPendingRequestHostIdPrefName,
                                  initial_pending_host_request);

    auto mock_timer = std::make_unique<base::MockOneShotTimer>();
    mock_timer_ = mock_timer.get();

    delegate_ = HostBackendDelegateImpl::Factory::Create(
        fake_eligible_host_devices_provider_.get(), test_pref_service_.get(),
        fake_device_sync_client_.get(), std::move(mock_timer));
    EXPECT_EQ(initial_host, delegate_->GetMultiDeviceHostFromBackend());

    observer_ = std::make_unique<FakeHostBackendDelegateObserver>();
    delegate_->AddObserver(observer_.get());
  }

  int GetSetHostNetworkRequestCallbackQueueSize() {
    return features::ShouldUseV1DeviceSync()
               ? fake_device_sync_client_
                     ->GetSetSoftwareFeatureStateInputsQueueSize()
               : fake_device_sync_client_->GetSetFeatureStatusInputsQueueSize();
  }

  void InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult result_code,
      bool expected_to_notify_observer_and_start_retry_timer) {
    size_t num_failure_events_before_call =
        observer_->num_failed_backend_requests();

    if (features::ShouldUseV1DeviceSync()) {
      fake_device_sync_client_->InvokePendingSetSoftwareFeatureStateCallback(
          result_code);
    } else {
      fake_device_sync_client_->InvokePendingSetFeatureStatusCallback(
          result_code);
    }

    if (expected_to_notify_observer_and_start_retry_timer) {
      EXPECT_EQ(num_failure_events_before_call + 1u,
                observer_->num_failed_backend_requests());
    } else {
      EXPECT_EQ(num_failure_events_before_call,
                observer_->num_failed_backend_requests());
    }

    EXPECT_EQ(expected_to_notify_observer_and_start_retry_timer,
              mock_timer_->IsRunning());
  }

  void SimulateNewHostDevicesSynced(
      const std::optional<multidevice::RemoteDeviceRef>& host_device_after_sync,
      bool expected_to_fulfill_pending_request) {
    std::optional<multidevice::RemoteDeviceRef> host_device_before_call =
        delegate_->GetMultiDeviceHostFromBackend();
    bool host_changed = host_device_before_call != host_device_after_sync;
    size_t num_host_change_events_before_call =
        observer_->num_changes_on_backend();
    size_t num_pending_host_request_change_events_before_call =
        observer_->num_pending_host_request_changes();

    SetHostInDeviceSyncClient(host_device_after_sync);
    fake_device_sync_client_->NotifyNewDevicesSynced();

    if (host_changed) {
      EXPECT_EQ(num_host_change_events_before_call + 1u,
                observer_->num_changes_on_backend());
    } else {
      EXPECT_EQ(num_host_change_events_before_call,
                observer_->num_changes_on_backend());
    }

    if (expected_to_fulfill_pending_request) {
      EXPECT_FALSE(delegate_->HasPendingHostRequest());

      // Expected to change from a pending request to no request.
      EXPECT_EQ(num_pending_host_request_change_events_before_call + 1u,
                observer_->num_pending_host_request_changes());
    } else {
      EXPECT_EQ(num_pending_host_request_change_events_before_call,
                observer_->num_pending_host_request_changes());
    }
  }

  void AttemptToSetMultiDeviceHostOnBackend(
      const std::optional<multidevice::RemoteDeviceRef>& host_device) {
    std::optional<multidevice::RemoteDeviceRef> host_before_call =
        delegate_->GetMultiDeviceHostFromBackend();
    bool attempting_to_set_host_which_already_exists =
        host_device == host_before_call;
    size_t num_pending_host_request_change_events_before_call =
        observer_->num_pending_host_request_changes();
    bool was_request_for_same_device_as_pending_request =
        delegate_->HasPendingHostRequest() &&
        delegate_->GetPendingHostRequest() == host_device;

    delegate_->AttemptToSetMultiDeviceHostOnBackend(host_device);

    // A new attempt means that any previous retry attempts should have been
    // canceled.
    EXPECT_FALSE(mock_timer_->IsRunning());

    if (attempting_to_set_host_which_already_exists) {
      EXPECT_FALSE(delegate_->HasPendingHostRequest());
      return;
    }

    EXPECT_EQ(host_device, delegate_->GetPendingHostRequest());

    if (was_request_for_same_device_as_pending_request) {
      EXPECT_EQ(num_pending_host_request_change_events_before_call,
                observer_->num_pending_host_request_changes());
    } else {
      EXPECT_EQ(num_pending_host_request_change_events_before_call + 1u,
                observer_->num_pending_host_request_changes());
    }

    // Verify that the correct parameters were passed to
    // SetSoftwareFeatureState() or SetFeatureStatus().
    if (host_device) {
      VerifyLatestSetHostNetworkRequest(*host_device, true /* should_enable */);
    } else {
      ASSERT_TRUE(host_before_call);
      VerifyLatestSetHostNetworkRequest(*host_before_call,
                                        false /* should_enable */);
    }
  }

  void SetHostInDeviceSyncClient(
      const std::optional<multidevice::RemoteDeviceRef>& host_device) {
    for (const auto& remote_device : test_devices_) {
      bool should_be_host =
          host_device != std::nullopt &&
          ((!remote_device.instance_id().empty() &&
            host_device->instance_id() == remote_device.instance_id()) ||
           (!remote_device.GetDeviceId().empty() &&
            host_device->GetDeviceId() == remote_device.GetDeviceId()));

      GetMutableRemoteDevice(remote_device)
          ->software_features
              [multidevice::SoftwareFeature::kBetterTogetherHost] =
          should_be_host ? multidevice::SoftwareFeatureState::kEnabled
                         : multidevice::SoftwareFeatureState::kSupported;
    }
  }

  FakeEligibleHostDevicesProvider* fake_eligible_host_devices_provider() {
    return fake_eligible_host_devices_provider_.get();
  }

  device_sync::FakeDeviceSyncClient* fake_device_sync_client() {
    return fake_device_sync_client_.get();
  }

  FakeHostBackendDelegateObserver* observer() { return observer_.get(); }

  base::MockOneShotTimer* mock_timer() { return mock_timer_; }

  HostBackendDelegate* delegate() { return delegate_.get(); }

  const multidevice::RemoteDeviceRefList& test_devices() const {
    return test_devices_;
  }

 private:
  void SetFeatureFlags(bool use_v1_devicesync) {
    std::vector<base::test::FeatureRef> enabled_features;
    std::vector<base::test::FeatureRef> disabled_features;

    // These flags have no direct effect of on the host backend delegate;
    // however, v2 Enrollment and DeviceSync must be enabled before v1
    // DeviceSync can be disabled.
    enabled_features.push_back(features::kCryptAuthV2Enrollment);
    enabled_features.push_back(features::kCryptAuthV2DeviceSync);

    if (use_v1_devicesync) {
      disabled_features.push_back(features::kDisableCryptAuthV1DeviceSync);
    } else {
      enabled_features.push_back(features::kDisableCryptAuthV1DeviceSync);
    }

    scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
  }

  void VerifyLatestSetHostNetworkRequest(
      const multidevice::RemoteDeviceRef expected_host,
      bool expected_should_enable) {
    // Verify inputs to SetSoftwareFeatureState().
    if (features::ShouldUseV1DeviceSync()) {
      ASSERT_FALSE(
          fake_device_sync_client_->set_software_feature_state_inputs_queue()
              .empty());
      const device_sync::FakeDeviceSyncClient::SetSoftwareFeatureStateInputs&
          inputs = fake_device_sync_client_
                       ->set_software_feature_state_inputs_queue()
                       .back();
      EXPECT_EQ(expected_host.public_key(), inputs.public_key);
      EXPECT_EQ(multidevice::SoftwareFeature::kBetterTogetherHost,
                inputs.software_feature);
      EXPECT_EQ(expected_should_enable, inputs.enabled);
      EXPECT_EQ(expected_should_enable, inputs.is_exclusive);
      return;
    }

    // Verify inputs to SetFeatureStatus().
    ASSERT_FALSE(
        fake_device_sync_client_->set_feature_status_inputs_queue().empty());
    const device_sync::FakeDeviceSyncClient::SetFeatureStatusInputs& inputs =
        fake_device_sync_client_->set_feature_status_inputs_queue().back();
    EXPECT_EQ(expected_host.instance_id(), inputs.device_instance_id);
    EXPECT_EQ(multidevice::SoftwareFeature::kBetterTogetherHost,
              inputs.feature);
    EXPECT_EQ(expected_should_enable
                  ? device_sync::FeatureStatusChange::kEnableExclusively
                  : device_sync::FeatureStatusChange::kDisable,
              inputs.status_change);
  }

  multidevice::RemoteDeviceRefList test_devices_;

  std::unique_ptr<FakeEligibleHostDevicesProvider>
      fake_eligible_host_devices_provider_;
  std::unique_ptr<sync_preferences::TestingPrefServiceSyncable>
      test_pref_service_;
  std::unique_ptr<device_sync::FakeDeviceSyncClient> fake_device_sync_client_;
  raw_ptr<base::MockOneShotTimer, DanglingUntriaged> mock_timer_;

  std::unique_ptr<FakeHostBackendDelegateObserver> observer_;

  std::unique_ptr<HostBackendDelegate> delegate_;

  base::test::ScopedFeatureList scoped_feature_list_;
};

TEST_P(MultiDeviceSetupHostBackendDelegateImplTest, Success) {
  CreateDelegate(std::nullopt /* initial_host */);

  // Set device 0.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[0]);
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kSuccess,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetPendingHostRequest());
  SimulateNewHostDevicesSynced(test_devices()[0] /* host_device_after_sync */,
                               true /* expected_to_fulfill_pending_request */);
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetMultiDeviceHostFromBackend());

  // Remove device 0 such that there is no longer a host.
  AttemptToSetMultiDeviceHostOnBackend(std::nullopt);
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kSuccess,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(std::nullopt, delegate()->GetPendingHostRequest());
  SimulateNewHostDevicesSynced(std::nullopt /* host_device_after_sync */,
                               true /* expected_to_fulfill_pending_request */);
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(std::nullopt, delegate()->GetMultiDeviceHostFromBackend());

  // Set device 1.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[1]);
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kSuccess,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[1], delegate()->GetPendingHostRequest());
  SimulateNewHostDevicesSynced(test_devices()[1] /* host_device_after_sync */,
                               true /* expected_to_fulfill_pending_request */);
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[1], delegate()->GetMultiDeviceHostFromBackend());
}

TEST_P(MultiDeviceSetupHostBackendDelegateImplTest, Failure) {
  CreateDelegate(std::nullopt /* initial_host */);

  // Attempt to set device 0, but fail.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[0]);
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kOffline,
      true /* expected_to_notify_observer_and_start_retry_timer */);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetPendingHostRequest());
  EXPECT_EQ(std::nullopt, delegate()->GetMultiDeviceHostFromBackend());

  // A retry should have been scheduled, so fire the timer to start the retry.
  mock_timer()->Fire();

  // Simulate another failure.
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kOffline,
      true /* expected_to_notify_observer_and_start_retry_timer */);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetPendingHostRequest());
  EXPECT_EQ(std::nullopt, delegate()->GetMultiDeviceHostFromBackend());

  // Attempt to set device 1, but fail.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[1]);
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kOffline,
      true /* expected_to_notify_observer_and_start_retry_timer */);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[1], delegate()->GetPendingHostRequest());
  EXPECT_EQ(std::nullopt, delegate()->GetMultiDeviceHostFromBackend());
}

TEST_P(MultiDeviceSetupHostBackendDelegateImplTest,
       StartWithDevice_SimultaneousRequests) {
  // Start with device 0 as the active host.
  CreateDelegate(test_devices()[0] /* initial_host */);

  // Attempt to set device 1, but do not invoke the callback yet.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[1]);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[1], delegate()->GetPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetMultiDeviceHostFromBackend());

  // Attempt to set device 2, but do not invoke device 1's callback yet.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[2]);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[2], delegate()->GetPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetMultiDeviceHostFromBackend());

  // Attempt to set device 3.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[3]);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[3], delegate()->GetPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetMultiDeviceHostFromBackend());

  // Note: Below, we assume that the feature setting requests are processed in
  // the order they are called. This is an assumption made in the
  // HostBackendDelegate implementation.

  // Fire the callback for device 1, but have it fail. This is not expected to
  // notify the observer or start the retry timer, since the failure was for
  // device 1's request and device 3 is the pending host request.
  EXPECT_EQ(3, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kOffline,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[3], delegate()->GetPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetMultiDeviceHostFromBackend());

  // Fire the callback for device 2, and have it succeed. This should affect the
  // value of GetMultiDeviceHostFromBackend(), but there should still be a
  // pending request for device 3.
  EXPECT_EQ(2, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kSuccess,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  SimulateNewHostDevicesSynced(test_devices()[2] /* host_device_after_sync */,
                               false /* expected_to_fulfill_pending_request */);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[3], delegate()->GetPendingHostRequest());
  EXPECT_EQ(test_devices()[2], delegate()->GetMultiDeviceHostFromBackend());

  // Fire the callback for device 3, and have it succeed.
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kSuccess,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  SimulateNewHostDevicesSynced(test_devices()[3] /* host_device_after_sync */,
                               true /* expected_to_fulfill_pending_request */);
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[3], delegate()->GetMultiDeviceHostFromBackend());
}

TEST_P(MultiDeviceSetupHostBackendDelegateImplTest,
       SimultaneousRequestsToSameDevice) {
  CreateDelegate(std::nullopt /* initial_host */);

  // Attempt to set device 0, but do not invoke the callback yet.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[0]);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetPendingHostRequest());
  EXPECT_EQ(std::nullopt, delegate()->GetMultiDeviceHostFromBackend());

  // Attempt to set device 0 again, and still do not invoke the callback.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[0]);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetPendingHostRequest());
  EXPECT_EQ(std::nullopt, delegate()->GetMultiDeviceHostFromBackend());

  // Attempt to set device 0 one more time.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[0]);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetPendingHostRequest());
  EXPECT_EQ(std::nullopt, delegate()->GetMultiDeviceHostFromBackend());

  // Fire the first callback, which should successfully transition the host.
  EXPECT_EQ(3, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kSuccess,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  SimulateNewHostDevicesSynced(test_devices()[0] /* host_device_after_sync */,
                               true /* expected_to_fulfill_pending_request */);
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetMultiDeviceHostFromBackend());

  // Fire the second callback, but have it fail. No state should be affected.
  EXPECT_EQ(2, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kOffline,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetMultiDeviceHostFromBackend());

  // Fire the third callback, and have it succeed. Still, no state should be
  // affected.
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kSuccess,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetMultiDeviceHostFromBackend());
}

TEST_P(MultiDeviceSetupHostBackendDelegateImplTest,
       MultipleRequestsToSameDevice_FirstFail_ThenSucceed) {
  CreateDelegate(std::nullopt /* initial_host */);

  // Attempt to set device 0, but fail.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[0]);
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kOffline,
      true /* expected_to_notify_observer_and_start_retry_timer */);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetPendingHostRequest());
  EXPECT_EQ(std::nullopt, delegate()->GetMultiDeviceHostFromBackend());

  // The retry timer is running; however, instead of relying on that, call
  // AttemptToSetMultiDeviceHostOnBackend() again to trigger an immediate retry
  // without the timer.
  AttemptToSetMultiDeviceHostOnBackend(test_devices()[0]);
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kSuccess,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  EXPECT_TRUE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetPendingHostRequest());
  SimulateNewHostDevicesSynced(test_devices()[0] /* host_device_after_sync */,
                               true /* expected_to_fulfill_pending_request */);
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetMultiDeviceHostFromBackend());
}

TEST_P(MultiDeviceSetupHostBackendDelegateImplTest,
       InitialPendingRequestButNoInitialDevice) {
  CreateDelegate(
      std::nullopt /* initial_host */,
      features::ShouldUseV1DeviceSync()
          ? test_devices()[0].GetDeviceId()
          : test_devices()[0].instance_id() /* initial_pending_host_request */);

  // The delegate should have started a request as soon as it was created.
  // Simulate it succeeding.
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kSuccess,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  SimulateNewHostDevicesSynced(test_devices()[0] /* host_device_after_sync */,
                               true /* expected_to_fulfill_pending_request */);
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(test_devices()[0], delegate()->GetMultiDeviceHostFromBackend());
}

TEST_P(MultiDeviceSetupHostBackendDelegateImplTest,
       InitialDeviceWithPendingRequestToRemoveIt) {
  CreateDelegate(
      test_devices()[0] /* initial_host */,
      kPendingRemovalOfCurrentHost /* initial_pending_host_request */);

  // The delegate should have started a request as soon as it was created.
  // Simulate it succeeding.
  EXPECT_EQ(1, GetSetHostNetworkRequestCallbackQueueSize());
  InvokePendingSetHostNetworkRequestCallback(
      device_sync::mojom::NetworkRequestResult::kSuccess,
      false /* expected_to_notify_observer_and_start_retry_timer */);
  SimulateNewHostDevicesSynced(std::nullopt /* host_device_after_sync */,
                               true /* expected_to_fulfill_pending_request */);
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
  EXPECT_EQ(std::nullopt, delegate()->GetMultiDeviceHostFromBackend());
}

TEST_P(MultiDeviceSetupHostBackendDelegateImplTest, ChangedFromOtherDevice) {
  CreateDelegate(std::nullopt /* initial_host */);

  // The device changed from another device (i.e.,
  // AttemptToSetMultiDeviceHostOnBackend() was not called).
  SimulateNewHostDevicesSynced(test_devices()[0] /* host_device_after_sync */,
                               false /* expected_to_fulfill_pending_request */);

  // One more change.
  SimulateNewHostDevicesSynced(test_devices()[1] /* host_device_after_sync */,
                               false /* expected_to_fulfill_pending_request */);
}

TEST_P(MultiDeviceSetupHostBackendDelegateImplTest,
       PendingRequestCanceledIfDeviceToSetNoLongerExists) {
  CreateDelegate(std::nullopt /* initial_host */,
                 "nonexistentDeviceId" /* initial_pending_host_request */);

  // An initial pending host request exists, but it is for a host that is not
  // present in the DeviceSyncClient. Thus, the request should be canceled.
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
}

TEST_P(MultiDeviceSetupHostBackendDelegateImplTest,
       PendingRequestCanceledIfDeviceToRemoveNoLongerExists) {
  CreateDelegate(
      std::nullopt /* initial_host */,
      kPendingRemovalOfCurrentHost /* initial_pending_host_request */);

  // An initial pending host request exists to remove the current host, but
  // there actually is no current host. Thus, the request should be canceled.
  EXPECT_FALSE(delegate()->HasPendingHostRequest());
}

TEST_P(MultiDeviceSetupHostBackendDelegateImplTest, TryToSetNonEligibleHost) {
  // Make all test devices ineligible.
  fake_eligible_host_devices_provider()->set_eligible_host_devices(
      multidevice::RemoteDeviceRefList());

  CreateDelegate(std::nullopt /* initial_host */);

  delegate()->AttemptToSetMultiDeviceHostOnBackend(test_devices()[0]);
  EXPECT_EQ(0u, observer()->num_pending_host_request_changes());
}

// Runs tests twice; once with v1 DeviceSync enabled and once with it disabled.
// TODO(crbug.com/40105247): Remove when v1 DeviceSync is disabled,
// when all devices should have an Instance ID.
INSTANTIATE_TEST_SUITE_P(All,
                         MultiDeviceSetupHostBackendDelegateImplTest,
                         ::testing::Bool());

}  // namespace multidevice_setup

}  // namespace ash