chromium/ash/webui/shimless_rma/resources/fake_shimless_rma_service.ts

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

import {assert} from 'chrome://resources/ash/common/assert.js';
import {FakeMethodResolver} from 'chrome://resources/ash/common/fake_method_resolver.js';
import {FakeObservables} from 'chrome://resources/ash/common/fake_observables.js';
import {FilePath} from 'chrome://resources/mojo/mojo/public/mojom/base/file_path.mojom-webui.js';

import {CalibrationComponentStatus, CalibrationObserverRemote, CalibrationOverallStatus, CalibrationSetupInstruction, CalibrationStatus, Component, ComponentType, ErrorObserverRemote, ExternalDiskStateObserverRemote, FeatureLevel, FinalizationError, FinalizationObserverRemote, FinalizationStatus, HardwareVerificationStatusObserverRemote, HardwareWriteProtectionStateObserverRemote, OsUpdateObserverRemote, OsUpdateOperation, PowerCableStateObserverRemote, ProvisioningError, ProvisioningObserverRemote, ProvisioningStatus, RmadErrorCode, Shimless3pDiagnosticsAppInfo, ShimlessRmaServiceInterface, Show3pDiagnosticsAppResult, ShutdownMethod, State, StateResult, UpdateErrorCode, UpdateRoFirmwareObserverRemote, UpdateRoFirmwareStatus, WriteProtectDisableCompleteAction} from './shimless_rma.mojom-webui.js';


/**
 * Type for methods needed for the fake FakeShimlessRmaService implementation.
 */
export type FakeShimlessRmaServiceInterface = ShimlessRmaServiceInterface&{
  setStates(states: StateResult[]): void,
  setAsyncOperationDelayMs(delayMs: number): void,
  setAbortRmaResult(error: RmadErrorCode): void,
  enableAutomaticallyTriggerProvisioningObservation(): void,
  getCurrentOsVersion(): void,
  setCheckForOsUpdatesResult(version: string): void,
  setUpdateOsResult(started: boolean): void,
  setGetRsuDisableWriteProtectChallengeResult(challenge: string): void,
  enableAutomaticallyTriggerDisableWriteProtectionObservation(): void,
  setGetPowerwashRequiredResult(powerwashRequired: boolean): void,
  setSaveLogResult(savePath: FilePath): void,
  enableAutomaticallyTriggerHardwareVerificationStatusObservation(): void,
  setGetCurrentOsVersionResult(version: string|null): void,
  setGetComponentListResult(components: Component[]): void,
  setGetRsuDisableWriteProtectHwidResult(hwid: string): void,
  getRsuDisableWriteProtectChallengeQrCode(): Promise<{qrCodeData: number[]}>,
  setGetRsuDisableWriteProtectChallengeQrCodeResponse(qrCodeData: number[]):
      void,
  setGetLogResult(log: string): void,
  enableAutomaticallyTriggerFinalizationObservation(): void,
  enableAutomaticallyTriggerOsUpdateObservation(): void,
  setGetWriteProtectDisableCompleteAction(
      action: WriteProtectDisableCompleteAction): void,
  getWriteProtectDisableCompleteAction():
      Promise<{action: WriteProtectDisableCompleteAction}>,
  getOriginalSerialNumber(): Promise<{serialNumber: string}>,
  setGetOriginalSerialNumberResult(serialNumber: string): void,
  getRegionList(): Promise<{regions: string[]}>,
  setGetRegionListResult(regions: string[]): void,
  getCalibrationComponentList():
      Promise<{components: CalibrationComponentStatus[]}>,
  setGetCalibrationComponentListResult(
      components: CalibrationComponentStatus[]): void,
  setGetSkuListResult(skus: bigint[]): void,
  setGetSkuDescriptionListResult(skuDescriptions: string[]): void,
  enableAautomaticallyTriggerUpdateRoFirmwareObservation(): void,
  setGetOriginalSkuResult(skuIndex: number): void,
  enableAutomaticallyTriggerCalibrationObservation(): void,
  getCustomLabelList(): Promise<{customLabels: string[]}>,
  enableAutomaticallyTriggerPowerCableStateObservation(): void,
  setGetOriginalRegionResult(regionIndex: number): void,
  setGetCustomLabelListResult(customLabels: string[]): void,
  setGetOriginalCustomLabelResult(customLabelIndex: number): void,
  getOriginalDramPartNumber(): Promise<{dramPartNumber: string}>,
  setGetOriginalDramPartNumberResult(dramPartNumber: string): void,
  setGetOriginalFeatureLevelResult(featureLevel: FeatureLevel): void,
  setGetCalibrationSetupInstructionsResult(
      instructions: CalibrationSetupInstruction): void,
};


export class FakeShimlessRmaService implements FakeShimlessRmaServiceInterface {
  constructor() {
    this.methods = new FakeMethodResolver();
    this.observables = new FakeObservables();

    /**
     * The list of states for this RMA flow.
     */
    this.states = [];

    /**
     * The index into states for the current fake state.
     */
    this.stateIndex = 0;

    /**
     * The list of components.
     */
    this.components = [];

    /**
     * Control automatically triggering a HWWP disable observation.
     */
    this.automaticallyTriggerDisableWriteProtectionObservation = false;

    /**
     * Control automatically triggering update RO firmware observations.
     */
    this.automaticallyTriggerUpdateRoFirmwareObservation = false;

    /**
     * Control automatically triggering provisioning observations.
     */
    this.automaticallyTriggerProvisioningObservation = false;

    /**
     * Control automatically triggering calibration observations.
     */
    this.automaticallyTriggerCalibrationObservation = false;

    /**
     * Control automatically triggering OS update observations.
     */
    this.automaticallyTriggerOsUpdateObservation = false;

    /**
     * Control automatically triggering a hardware verification observation.
     */
    this.automaticallyTriggerHardwareVerificationStatusObservation = false;

    /**
     * Control automatically triggering a finalization observation.
     */
    this.automaticallyTriggerFinalizationObservation = false;

    /**
     * Control automatically triggering power cable state observations.
     */
    this.automaticallyTriggerPowerCableStateObservation = false;

    /**
     * Both abortRma and forward state transitions can have significant delays
     * that are useful to fake for manual testing.
     * Defaults to no delay for unit tests.
     */
    this.resolveMethodDelayMs = 0;

    /**
     * The result of calling trackConfiguredNetworks.
     */
    this.trackConfiguredNetworksCalled = false;

    /**
     * The approval of last call to completeLast3pDiagnosticsInstallation.
     */
    this.lastCompleteLast3pDiagnosticsInstallationApproval = null;

    /**
     * Has show3pDiagnosticsApp been called.
     */
    this.wasShow3pDiagnosticsAppCalled = false;

    this.reset();
  }

  private methods: FakeMethodResolver;
  private observables: FakeObservables;
  private states: StateResult[];
  private stateIndex: number;
  private components: Component[];
  private automaticallyTriggerDisableWriteProtectionObservation: boolean;
  private automaticallyTriggerUpdateRoFirmwareObservation: boolean;
  private automaticallyTriggerProvisioningObservation: boolean;
  private automaticallyTriggerCalibrationObservation: boolean;
  private automaticallyTriggerOsUpdateObservation: boolean;
  private automaticallyTriggerHardwareVerificationStatusObservation: boolean;
  private automaticallyTriggerFinalizationObservation: boolean;
  private automaticallyTriggerPowerCableStateObservation: boolean;
  private resolveMethodDelayMs: number;
  private trackConfiguredNetworksCalled: boolean;
  private lastCompleteLast3pDiagnosticsInstallationApproval: boolean|null;
  private wasShow3pDiagnosticsAppCalled: boolean;

  setAsyncOperationDelayMs(delayMs: number) {
    this.resolveMethodDelayMs = delayMs;
  }

  /**
   * Set the ordered list of states end error codes for this fake.
   * Setting an empty list (the default) returns kRmaNotRequired for any state
   * function.
   * Next state functions and transitionPreviousState will move through the fake
   * state through the list, and return kTransitionFailed if it would move off
   * either end. getCurrentState always return the state at the current index.
   */
  setStates(states: StateResult[]) {
    this.states = states;
    this.stateIndex = 0;
  }

  getCurrentState(): Promise<{stateResult: StateResult}> {
    // As next state functions and transitionPreviousState can modify the result
    // of this function the result must be set at the time of the call.
    if (this.states.length === 0) {
      this.setFakeCurrentState(
          State.kUnknown, false, false, RmadErrorCode.kRmaNotRequired);
    } else {
      // It should not be possible for stateIndex to be out of range unless
      // there is a bug in the fake.
      assert(this.stateIndex < this.states.length);
      const state = this.states[this.stateIndex];
      this.setFakeCurrentState(
          state.state, state.canExit, state.canGoBack, state.error);
    }
    return this.methods.resolveMethodWithDelay(
        'getCurrentState', this.resolveMethodDelayMs);
  }

  transitionPreviousState(): Promise<{stateResult: StateResult}> {
    // As next state methods and transitionPreviousState can modify the result
    // of this function the result must be set at the time of the call.
    if (this.states.length === 0) {
      this.setFakePrevState(
          State.kUnknown, false, false, RmadErrorCode.kRmaNotRequired);
    } else if (this.stateIndex === 0) {
      // It should not be possible for stateIndex to be out of range unless
      // there is a bug in the fake.
      assert(this.stateIndex < this.states.length);
      const state = this.states[this.stateIndex];
      this.setFakePrevState(
          state.state, state.canExit, state.canGoBack,
          RmadErrorCode.kTransitionFailed);
    } else {
      this.stateIndex--;
      const state = this.states[this.stateIndex];
      this.setFakePrevState(
          state.state, state.canExit, state.canGoBack, state.error);
    }
    return this.methods.resolveMethodWithDelay(
        'transitionPreviousState', this.resolveMethodDelayMs);
  }

  abortRma(): Promise<{error: RmadErrorCode}> {
    return this.methods.resolveMethodWithDelay(
        'abortRma', this.resolveMethodDelayMs);
  }

  /**
   * Sets the value that will be returned when calling abortRma().
   */
  setAbortRmaResult(error: RmadErrorCode): void {
    this.methods.setResult('abortRma', {error: error});
  }

  beginFinalization(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'beginFinalization', State.kWelcomeScreen);
  }

  trackConfiguredNetworks(): void {
    this.trackConfiguredNetworksCalled = true;
  }

  getTrackConfiguredNetworks(): boolean {
    return this.trackConfiguredNetworksCalled;
  }

  networkSelectionComplete(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'networkSelectionComplete', State.kConfigureNetwork);
  }

  getCurrentOsVersion(): Promise<{version: string}> {
    return this.methods.resolveMethod('getCurrentOsVersion');
  }

  setGetCurrentOsVersionResult(version: string|null) {
    this.methods.setResult('getCurrentOsVersion', {version: version});
  }

  checkForOsUpdates(): Promise<{updateAvailable: boolean, version: string}> {
    return this.methods.resolveMethod('checkForOsUpdates');
  }

  setCheckForOsUpdatesResult(version: string) {
    this.methods.setResult(
        'checkForOsUpdates', {updateAvailable: true, version});
  }

  updateOs(): Promise<{updateStarted: boolean}> {
    if (this.automaticallyTriggerOsUpdateObservation) {
      this.triggerOsUpdateObserver(
          OsUpdateOperation.kCheckingForUpdate, 0.1, UpdateErrorCode.kSuccess,
          500);
      this.triggerOsUpdateObserver(
          OsUpdateOperation.kUpdateAvailable, 0.3, UpdateErrorCode.kSuccess,
          1000);
      this.triggerOsUpdateObserver(
          OsUpdateOperation.kDownloading, 0.5, UpdateErrorCode.kSuccess, 1500);
      this.triggerOsUpdateObserver(
          OsUpdateOperation.kVerifying, 0.7, UpdateErrorCode.kSuccess, 2000);
      this.triggerOsUpdateObserver(
          OsUpdateOperation.kFinalizing, 0.9, UpdateErrorCode.kSuccess, 2500);
      this.triggerOsUpdateObserver(
          OsUpdateOperation.kUpdatedNeedReboot, 1.0, UpdateErrorCode.kSuccess,
          3000);
    }
    return this.methods.resolveMethod('updateOs');
  }

  setUpdateOsResult(started: boolean): void {
    this.methods.setResult('updateOs', {updateStarted: started});
  }

  updateOsSkipped(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod('updateOsSkipped', State.kUpdateOs);
  }

  setSameOwner(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod('setSameOwner', State.kChooseDestination);
  }

  setDifferentOwner(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'setDifferentOwner', State.kChooseDestination);
  }

  setWipeDevice(_shouldWipeDevice: boolean):
      Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod('setWipeDevice', State.kChooseWipeDevice);
  }

  manualDisableWriteProtectAvailable(): Promise<{available: boolean}> {
    return this.methods.resolveMethod('manualDisableWriteProtectAvailable');
  }

  setManualDisableWriteProtectAvailableResult(available: boolean) {
    this.methods.setResult(
        'manualDisableWriteProtectAvailable', {available: available});
  }

  chooseManuallyDisableWriteProtect(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'chooseManuallyDisableWriteProtect',
        State.kChooseWriteProtectDisableMethod);
  }

  chooseRsuDisableWriteProtect(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'chooseRsuDisableWriteProtect', State.kChooseWriteProtectDisableMethod);
  }

  getRsuDisableWriteProtectChallenge(): Promise<{challenge: string}> {
    return this.methods.resolveMethod('getRsuDisableWriteProtectChallenge');
  }

  setGetRsuDisableWriteProtectChallengeResult(challenge: string) {
    this.methods.setResult(
        'getRsuDisableWriteProtectChallenge', {challenge: challenge});
  }

  getRsuDisableWriteProtectHwid(): Promise<{hwid: string}> {
    return this.methods.resolveMethod('getRsuDisableWriteProtectHwid');
  }

  setGetRsuDisableWriteProtectHwidResult(hwid: string) {
    this.methods.setResult('getRsuDisableWriteProtectHwid', {hwid: hwid});
  }

  getRsuDisableWriteProtectChallengeQrCode(): Promise<{qrCodeData: number[]}> {
    return this.methods.resolveMethod(
        'getRsuDisableWriteProtectChallengeQrCode');
  }

  setGetRsuDisableWriteProtectChallengeQrCodeResponse(qrCodeData: number[]) {
    this.methods.setResult(
        'getRsuDisableWriteProtectChallengeQrCode', {qrCodeData: qrCodeData});
  }

  setRsuDisableWriteProtectCode(_code: string):
      Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'setRsuDisableWriteProtectCode', State.kEnterRSUWPDisableCode);
  }

  writeProtectManuallyDisabled(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'writeProtectManuallyDisabled', State.kWaitForManualWPDisable);
  }

  getWriteProtectDisableCompleteAction():
      Promise<{action: WriteProtectDisableCompleteAction}> {
    return this.methods.resolveMethod('getWriteProtectDisableCompleteAction');
  }

  setGetWriteProtectDisableCompleteAction(
      action: WriteProtectDisableCompleteAction): void {
    this.methods.setResult(
        'getWriteProtectDisableCompleteAction', {action: action});
  }

  confirmManualWpDisableComplete(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'confirmManualWpDisableComplete', State.kWPDisableComplete);
  }

  getComponentList(): Promise<{components: Component[]}> {
    this.methods.setResult('getComponentList', {components: this.components});
    return this.methods.resolveMethod('getComponentList');
  }

  setGetComponentListResult(components: Component[]): void {
    this.components = components;
  }

  setComponentList(_components: Component[]):
      Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'setComponentList', State.kSelectComponents);
  }

  reworkMainboard(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'reworkMainboard', State.kSelectComponents);
  }

  roFirmwareUpdateComplete(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'roFirmwareUpdateComplete', State.kUpdateRoFirmware);
  }

  shutdownForRestock(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod('shutdownForRestock', State.kRestock);
  }

  continueFinalizationAfterRestock(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'continueFinalizationAfterRestock', State.kRestock);
  }

  getRegionList(): Promise<{regions: string[]}> {
    return this.methods.resolveMethod('getRegionList');
  }

  setGetRegionListResult(regions: string[]): void {
    this.methods.setResult('getRegionList', {regions: regions});
  }

  getSkuList(): Promise<{skus: bigint[]}> {
    return this.methods.resolveMethod('getSkuList');
  }

  setGetSkuListResult(skus: bigint[]): void {
    this.methods.setResult('getSkuList', {skus: skus});
  }

  getCustomLabelList(): Promise<{customLabels: string[]}> {
    return this.methods.resolveMethod('getCustomLabelList');
  }

  setGetCustomLabelListResult(customLabels: string[]): void {
    this.methods.setResult('getCustomLabelList', {customLabels: customLabels});
  }

  getSkuDescriptionList(): Promise<{skuDescriptions: string[]}> {
    return this.methods.resolveMethod('getSkuDescriptionList');
  }

  setGetSkuDescriptionListResult(skuDescriptions: string[]): void {
    this.methods.setResult(
        'getSkuDescriptionList', {skuDescriptions: skuDescriptions});
  }

  getOriginalSerialNumber(): Promise<{serialNumber: string}> {
    return this.methods.resolveMethod('getOriginalSerialNumber');
  }

  setGetOriginalSerialNumberResult(serialNumber: string): void {
    this.methods.setResult(
        'getOriginalSerialNumber', {serialNumber: serialNumber});
  }

  getOriginalRegion(): Promise<{regionIndex: number}> {
    return this.methods.resolveMethod('getOriginalRegion');
  }

  setGetOriginalRegionResult(regionIndex: number): void {
    this.methods.setResult('getOriginalRegion', {regionIndex: regionIndex});
  }

  getOriginalSku(): Promise<{skuIndex: number}> {
    return this.methods.resolveMethod('getOriginalSku');
  }

  setGetOriginalSkuResult(skuIndex: number): void {
    this.methods.setResult('getOriginalSku', {skuIndex: skuIndex});
  }

  getOriginalCustomLabel(): Promise<{customLabelIndex: number}> {
    return this.methods.resolveMethod('getOriginalCustomLabel');
  }

  setGetOriginalCustomLabelResult(customLabelIndex: number): void {
    this.methods.setResult(
        'getOriginalCustomLabel', {customLabelIndex: customLabelIndex});
  }

  getOriginalDramPartNumber(): Promise<{dramPartNumber: string}> {
    return this.methods.resolveMethod('getOriginalDramPartNumber');
  }

  setGetOriginalDramPartNumberResult(dramPartNumber: string): void {
    this.methods.setResult(
        'getOriginalDramPartNumber', {dramPartNumber: dramPartNumber});
  }

  getOriginalFeatureLevel(): Promise<{originalFeatureLevel: FeatureLevel}> {
    return this.methods.resolveMethod('getOriginalFeatureLevel');
  }

  setGetOriginalFeatureLevelResult(featureLevel: FeatureLevel): void {
    this.methods.setResult(
        'getOriginalFeatureLevel', {originalFeatureLevel: featureLevel});
  }

  setDeviceInformation(
    _serialNumber: string,
    _regionIndex: number,
    _skuIndex: number,
    _customLabelIndex: number,
    _dramPartNumber: string,
    _isChassisBranded: boolean,
    _hwComplianceVersion: number,
  ): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'setDeviceInformation', State.kUpdateDeviceInformation);
  }

  getCalibrationComponentList():
      Promise<{components: CalibrationComponentStatus[]}> {
    return this.methods.resolveMethod('getCalibrationComponentList');
  }

  setGetCalibrationComponentListResult(components:
                                           CalibrationComponentStatus[]): void {
    this.methods.setResult(
        'getCalibrationComponentList', {components: components});
  }

  getCalibrationSetupInstructions():
      Promise<{instructions: CalibrationSetupInstruction}> {
    return this.methods.resolveMethod('getCalibrationSetupInstructions');
  }

  setGetCalibrationSetupInstructionsResult(instructions:
                                               CalibrationSetupInstruction): void {
    this.methods.setResult(
        'getCalibrationSetupInstructions', {instructions: instructions});
  }

  /**
   * The fake does not use the status list parameter, the fake data is never
   * updated.
   */
  startCalibration(_components: CalibrationComponentStatus[]):
      Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'startCalibration', State.kCheckCalibration);
  }

  runCalibrationStep(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'runCalibrationStep', State.kSetupCalibration);
  }

  continueCalibration(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'continueCalibration', State.kRunCalibration);
  }

  calibrationComplete(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'calibrationComplete', State.kRunCalibration);
  }

  retryProvisioning(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'retryProvisioning', State.kProvisionDevice);
  }

  provisioningComplete(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'provisioningComplete', State.kProvisionDevice);
  }

  finalizationComplete(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod('finalizationComplete', State.kFinalize);
  }

  retryFinalization(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod('retryFinalization', State.kFinalize);
  }

  writeProtectManuallyEnabled(): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod(
        'writeProtectManuallyEnabled', State.kWaitForManualWPEnable);
  }

  getLog(): Promise<{log: string, error: RmadErrorCode}> {
    return this.methods.resolveMethod('getLog');
  }

  setGetLogResult(log: string): void {
    this.methods.setResult('getLog', {log: log, error: RmadErrorCode.kOk});
  }

  saveLog(): Promise<{savePath: FilePath, error: RmadErrorCode}> {
    return this.methods.resolveMethod('saveLog');
  }

  setSaveLogResult(savePath: FilePath): void {
    this.methods.setResult(
        'saveLog', {savePath: savePath, error: RmadErrorCode.kOk});
  }

  getPowerwashRequired():
      Promise<{powerwashRequired: boolean, error: RmadErrorCode}> {
    return this.methods.resolveMethod('getPowerwashRequired');
  }

  setGetPowerwashRequiredResult(powerwashRequired: boolean): void {
    this.methods.setResult(
        'getPowerwashRequired',
        {powerwashRequired: powerwashRequired, error: RmadErrorCode.kOk});
  }

  launchDiagnostics(): void {
    console.log('(Fake) Launching diagnostics...');
  }

  /**
   * The fake does not use the status list parameter, the fake data is never
   * updated.
   */
  endRma(_shutdownMethod: ShutdownMethod): Promise<{stateResult: StateResult}> {
    return this.getNextStateForMethod('endRma', State.kRepairComplete);
  }

  criticalErrorExitToLogin(): Promise<{error: RmadErrorCode}> {
    return this.methods.resolveMethodWithDelay(
        'criticalErrorExitToLogin', this.resolveMethodDelayMs);
  }

  criticalErrorReboot(): Promise<{error: RmadErrorCode}> {
    return this.methods.resolveMethodWithDelay(
        'criticalErrorReboot', this.resolveMethodDelayMs);
  }

  shutDownAfterHardwareError(): void {
    console.log('(Fake) Shutting down...');
  }

  get3pDiagnosticsProvider(): Promise<{provider: string | null}> {
    return this.methods.resolveMethodWithDelay(
        'get3pDiagnosticsProvider', this.resolveMethodDelayMs);
  }

  setGet3pDiagnosticsProviderResult(provider: string|null): void {
    this.methods.setResult('get3pDiagnosticsProvider', {provider});
  }

  getInstallable3pDiagnosticsAppPath(): Promise<{appPath: FilePath | null}> {
    return this.methods.resolveMethod('getInstallable3pDiagnosticsAppPath');
  }

  setInstallable3pDiagnosticsAppPath(appPath: FilePath|null): void {
    this.methods.setResult('getInstallable3pDiagnosticsAppPath', {appPath});
  }

  installLastFound3pDiagnosticsApp():
      Promise<{appInfo: Shimless3pDiagnosticsAppInfo | null}> {
    return this.methods.resolveMethod('installLastFound3pDiagnosticsApp');
  }

  setInstallLastFound3pDiagnosticsApp(appInfo: Shimless3pDiagnosticsAppInfo|
                                      null): void {
    this.methods.setResult('installLastFound3pDiagnosticsApp', {appInfo});
  }

  completeLast3pDiagnosticsInstallation(isApproved: boolean): Promise<void> {
    this.lastCompleteLast3pDiagnosticsInstallationApproval = isApproved;
    return Promise.resolve();
  }

  getLastCompleteLast3pDiagnosticsInstallationApproval(): boolean {
    assert(this.lastCompleteLast3pDiagnosticsInstallationApproval !== null);
    return this.lastCompleteLast3pDiagnosticsInstallationApproval as boolean;
  }

  show3pDiagnosticsApp(): Promise<{result: Show3pDiagnosticsAppResult}> {
    this.wasShow3pDiagnosticsAppCalled = true;
    return this.methods.resolveMethod('show3pDiagnosticsApp');
  }

  getWasShow3pDiagnosticsAppCalled(): boolean {
    return this.wasShow3pDiagnosticsAppCalled;
  }

  setShow3pDiagnosticsAppResult(result: Show3pDiagnosticsAppResult): void {
    this.methods.setResult('show3pDiagnosticsApp', {result});
  }

  /**
   * Implements ShimlessRmaServiceInterface.ObserveError.
   */
  observeError(remote: ErrorObserverRemote): void {
    this.observables.observe(
        'ErrorObserver_onError', (error: RmadErrorCode) => {
          remote.onError(error);
        });
  }

  /**
   * Implements ShimlessRmaServiceInterface.ObserveOsUpdate.
   */
  observeOsUpdateProgress(remote: OsUpdateObserverRemote): void {
    this.observables.observe(
        'OsUpdateObserver_onOsUpdateProgressUpdated',
        (operation: OsUpdateOperation, progress: number,
         errorCode: UpdateErrorCode) => {
          remote.onOsUpdateProgressUpdated(operation, progress, errorCode);
        });
  }

  /**
   * Implements ShimlessRmaServiceInterface.ObserveRoFirmwareUpdateProgress.
   */
  observeRoFirmwareUpdateProgress(remote: UpdateRoFirmwareObserverRemote): void {
    this.observables.observe(
        'UpdateRoFirmwareObserver_onUpdateRoFirmwareStatusChanged',
        (status: UpdateRoFirmwareStatus) => {
          remote.onUpdateRoFirmwareStatusChanged(status);
        });
    if (this.automaticallyTriggerUpdateRoFirmwareObservation) {
      this.triggerUpdateRoFirmwareObserver(UpdateRoFirmwareStatus.kWaitUsb, 0);
      this.triggerUpdateRoFirmwareObserver(
          UpdateRoFirmwareStatus.kUpdating, 1000);
      this.triggerUpdateRoFirmwareObserver(
          UpdateRoFirmwareStatus.kRebooting, 2000);
      this.triggerUpdateRoFirmwareObserver(
          UpdateRoFirmwareStatus.kComplete, 3000);
    }
  }

  /**
   * Trigger update ro firmware observations when an observer is added.
   */
  enableAautomaticallyTriggerUpdateRoFirmwareObservation(): void {
    this.automaticallyTriggerUpdateRoFirmwareObservation = true;
  }

  /**
   * Implements ShimlessRmaServiceInterface.ObserveCalibration.
   */
  observeCalibrationProgress(remote: CalibrationObserverRemote): void {
    this.observables.observe(
        'CalibrationObserver_onCalibrationUpdated',
        (componentStatus: CalibrationComponentStatus) => {
          remote.onCalibrationUpdated(componentStatus);
        });
    this.observables.observe(
        'CalibrationObserver_onCalibrationStepComplete',
        (status: CalibrationOverallStatus) => {
          remote.onCalibrationStepComplete(status);
        });
    if (this.automaticallyTriggerCalibrationObservation) {
      this.triggerCalibrationObserver(
          {
            component: ComponentType.kBaseAccelerometer,
            status: CalibrationStatus.kCalibrationWaiting,
            progress: 0.0,
          },
          1000);
      this.triggerCalibrationObserver(
          {
            component: ComponentType.kBaseAccelerometer,
            status: CalibrationStatus.kCalibrationInProgress,
            progress: 0.2,
          },
          2000);
      this.triggerCalibrationObserver(
          {
            component: ComponentType.kBaseAccelerometer,
            status: CalibrationStatus.kCalibrationInProgress,
            progress: 0.4,
          },
          3000);
      this.triggerCalibrationObserver(
          {
            component: ComponentType.kBaseAccelerometer,
            status: CalibrationStatus.kCalibrationInProgress,
            progress: 0.6,
          },
          4000);
      this.triggerCalibrationObserver(
          {
            component: ComponentType.kBaseAccelerometer,
            status: CalibrationStatus.kCalibrationInProgress,
            progress: 0.8,
          },
          5000);
      this.triggerCalibrationObserver(
          {
            component: ComponentType.kLidAccelerometer,
            status: CalibrationStatus.kCalibrationWaiting,
            progress: 0.0,
          },
          6000);
      this.triggerCalibrationObserver(
          {
            component: ComponentType.kBaseAccelerometer,
            status: CalibrationStatus.kCalibrationComplete,
            progress: 0.5,
          },
          7000);
      this.triggerCalibrationObserver(
          {
            component: ComponentType.kBaseAccelerometer,
            status: CalibrationStatus.kCalibrationFailed,
            progress: 1.0,
          },
          8000);
      this.triggerCalibrationObserver(
          {
            component: ComponentType.kBaseGyroscope,
            status: CalibrationStatus.kCalibrationSkip,
            progress: 1.0,
          },
          9000);
      this.triggerCalibrationOverallObserver(
          CalibrationOverallStatus.kCalibrationOverallComplete, 10000);
    }
  }

  /**
   * Implements ShimlessRmaServiceInterface.ObserveProvisioning.
   */
  observeProvisioningProgress(remote: ProvisioningObserverRemote): void {
    this.observables.observe(
        'ProvisioningObserver_onProvisioningUpdated',
        (status: ProvisioningStatus, progress: number,
         error: ProvisioningError) => {
          remote.onProvisioningUpdated(status, progress, error);
        });
    if (this.automaticallyTriggerProvisioningObservation) {
      // Fake progress over 4 seconds.
      this.triggerProvisioningObserver(
          ProvisioningStatus.kInProgress, 0.25, ProvisioningError.kUnknown,
          1000);
      this.triggerProvisioningObserver(
          ProvisioningStatus.kInProgress, 0.5, ProvisioningError.kUnknown,
          2000);
      this.triggerProvisioningObserver(
          ProvisioningStatus.kInProgress, 0.75, ProvisioningError.kUnknown,
          3000);
      this.triggerProvisioningObserver(
          ProvisioningStatus.kComplete, 1.0, ProvisioningError.kUnknown, 4000);
    }
  }

  /**
   * Trigger provisioning observations when an observer is added.
   */
  enableAutomaticallyTriggerProvisioningObservation(): void {
    this.automaticallyTriggerProvisioningObservation = true;
  }

  /**
   * Trigger calibration observations when an observer is added.
   */
  enableAutomaticallyTriggerCalibrationObservation(): void {
    this.automaticallyTriggerCalibrationObservation = true;
  }

  /**
   * Trigger OS update observations when an OS update is started.
   */
  enableAutomaticallyTriggerOsUpdateObservation(): void {
    this.automaticallyTriggerOsUpdateObservation = true;
  }

  /**
   * Implements ShimlessRmaServiceInterface.ObserveHardwareWriteProtectionState.
   */
  observeHardwareWriteProtectionState(
      remote: HardwareWriteProtectionStateObserverRemote): void {
    this.observables.observe(
        'HardwareWriteProtectionStateObserver_onHardwareWriteProtectionStateChanged',
        (enabled: boolean) => {
          remote.onHardwareWriteProtectionStateChanged(enabled);
        });
    if (this.states &&
        this.automaticallyTriggerDisableWriteProtectionObservation) {
      assert(this.stateIndex < this.states.length);
      this.triggerHardwareWriteProtectionObserver(
          this.states[this.stateIndex].state === State.kWaitForManualWPEnable,
          3000);
    }
  }

  /**
   * Trigger a disable write protection observation when an observer is added.
   */
  enableAutomaticallyTriggerDisableWriteProtectionObservation(): void {
    this.automaticallyTriggerDisableWriteProtectionObservation = true;
  }

  /**
   * Implements ShimlessRmaServiceInterface.ObservePowerCableState.
   */
  observePowerCableState(remote: PowerCableStateObserverRemote): void {
    this.observables.observe(
        'PowerCableStateObserver_onPowerCableStateChanged',
        (pluggedIn: boolean) => {
          remote.onPowerCableStateChanged(pluggedIn);
        });
    if (this.automaticallyTriggerPowerCableStateObservation) {
      this.triggerPowerCableObserver(false, 1000);
      this.triggerPowerCableObserver(true, 10000);
      this.triggerPowerCableObserver(false, 15000);
    }
  }

  /**
   * Trigger a disable power cable state observations when an observer is added.
   */
  enableAutomaticallyTriggerPowerCableStateObservation(): void {
    this.automaticallyTriggerPowerCableStateObservation = true;
  }

  /**
   * Implements ShimlessRmaServiceInterface.ObserveExternalDiskState.
   */
  observeExternalDiskState(remote: ExternalDiskStateObserverRemote): void {
    this.observables.observe(
        'ExternalDiskStateObserver_onExternalDiskStateChanged',
        (detected: boolean) => {
          remote.onExternalDiskStateChanged(detected);
        });

    this.triggerExternalDiskObserver(true, 10000);
    this.triggerExternalDiskObserver(false, 15000);
  }

  /**
   * Implements ShimlessRmaServiceInterface.ObserveHardwareVerificationStatus.
   */
  observeHardwareVerificationStatus(
      remote: HardwareVerificationStatusObserverRemote): void {
    this.observables.observe(
        'HardwareVerificationStatusObserver_onHardwareVerificationResult',
        (isCompliant: boolean, errorMessage: string) => {
          remote.onHardwareVerificationResult(isCompliant, errorMessage);
        });
    if (this.automaticallyTriggerHardwareVerificationStatusObservation) {
      this.triggerHardwareVerificationStatusObserver(true, '', 3000);
    }
  }


  /**
   * Trigger a hardware verification observation when an observer is added.
   */
  enableAutomaticallyTriggerHardwareVerificationStatusObservation(): void {
    this.automaticallyTriggerHardwareVerificationStatusObservation = true;
  }

  /**
   * Implements ShimlessRmaServiceInterface.ObserveFinalizationStatus.
   */
  observeFinalizationStatus(remote: FinalizationObserverRemote): void {
    this.observables.observe(
        'FinalizationObserver_onFinalizationUpdated',
        (status: FinalizationStatus, progress: number,
         error: FinalizationError) => {
          remote.onFinalizationUpdated(status, progress, error);
        });
    if (this.automaticallyTriggerFinalizationObservation) {
      this.triggerFinalizationObserver(
          FinalizationStatus.kInProgress, 0.25, FinalizationError.kUnknown,
          1000);
      this.triggerFinalizationObserver(
          FinalizationStatus.kInProgress, 0.75, FinalizationError.kUnknown,
          2000);
      this.triggerFinalizationObserver(
          FinalizationStatus.kComplete, 1.0, FinalizationError.kUnknown, 3000);
    }
  }

  /**
   * Trigger a finalization progress observation when an observer is added.
   */
  enableAutomaticallyTriggerFinalizationObservation(): void {
    this.automaticallyTriggerFinalizationObservation = true;
  }

  /**
   * Causes the error observer to fire after a delay.
   */
  triggerErrorObserver(error: RmadErrorCode, delayMs: number): Promise<unknown> {
    return this.triggerObserverAfterMs('ErrorObserver_onError', error, delayMs);
  }

  /**
   * Causes the OS update observer to fire after a delay.
   */
  triggerOsUpdateObserver(
      operation: OsUpdateOperation, progress: number, error: UpdateErrorCode,
      delayMs: number): Promise<unknown> {
    return this.triggerObserverAfterMs(
        'OsUpdateObserver_onOsUpdateProgressUpdated',
        [operation, progress, error], delayMs);
  }

  /**
   * Causes the update RO firmware observer to fire after a delay.
   */
  triggerUpdateRoFirmwareObserver(
      status: UpdateRoFirmwareStatus, delayMs: number): Promise<unknown> {
    return this.triggerObserverAfterMs(
        'UpdateRoFirmwareObserver_onUpdateRoFirmwareStatusChanged', status,
        delayMs);
  }

  /**
   * Causes the calibration observer to fire after a delay.
   */
  triggerCalibrationObserver(
      componentStatus: CalibrationComponentStatus, delayMs: number): Promise<unknown> {
    return this.triggerObserverAfterMs(
        'CalibrationObserver_onCalibrationUpdated', componentStatus, delayMs);
  }

  /**
   * Causes the calibration overall observer to fire after a delay.
   */
  triggerCalibrationOverallObserver(
      status: CalibrationOverallStatus, delayMs: number) {
    return this.triggerObserverAfterMs(
        'CalibrationObserver_onCalibrationStepComplete', status, delayMs);
  }

  /**
   * Causes the provisioning observer to fire after a delay.
   */
  triggerProvisioningObserver(
      status: ProvisioningStatus, progress: number, error: ProvisioningError,
      delayMs: number): Promise<unknown> {
    return this.triggerObserverAfterMs(
        'ProvisioningObserver_onProvisioningUpdated', [status, progress, error],
        delayMs);
  }

  /**
   * Causes the hardware write protection observer to fire after a delay.
   */
  triggerHardwareWriteProtectionObserver(enabled: boolean, delayMs: number): Promise<unknown> {
    return this.triggerObserverAfterMs(
        'HardwareWriteProtectionStateObserver_onHardwareWriteProtectionStateChanged',
        enabled, delayMs);
  }

  /**
   * Causes the power cable observer to fire after a delay.
   */
  triggerPowerCableObserver(pluggedIn: boolean, delayMs: number): Promise<unknown> {
    return this.triggerObserverAfterMs(
        'PowerCableStateObserver_onPowerCableStateChanged', pluggedIn, delayMs);
  }

  /**
   * Causes the external disk observer to fire after a delay.
   */
  triggerExternalDiskObserver(detected: boolean, delayMs: number): Promise<unknown> {
    return this.triggerObserverAfterMs(
        'ExternalDiskStateObserver_onExternalDiskStateChanged', detected,
        delayMs);
  }

  /**
   * Causes the hardware verification observer to fire after a delay.
   */
  triggerHardwareVerificationStatusObserver(
      isCompliant: boolean, errorMessage: string, delayMs: number): Promise<unknown> {
    return this.triggerObserverAfterMs(
        'HardwareVerificationStatusObserver_onHardwareVerificationResult',
        [isCompliant, errorMessage], delayMs);
  }

  /**
   * Causes the finalization observer to fire after a delay.
   */
  triggerFinalizationObserver(
      status: FinalizationStatus, progress: number, error: FinalizationError,
      delayMs: number): Promise<unknown> {
    return this.triggerObserverAfterMs(
        'FinalizationObserver_onFinalizationUpdated', [status, progress, error],
        delayMs);
  }

  /**
   * Causes an observer to fire after a delay.
   */
  triggerObserverAfterMs<T>(method: string, result: T, delayMs: number): Promise<unknown> {
    const setDataTriggerAndResolve = function(
        service: FakeShimlessRmaService, resolve: any) {
      service.observables.setObservableData(method, [result]);
      service.observables.trigger(method);
      resolve();
    };

    return new Promise((resolve) => {
      if (delayMs === 0) {
        setDataTriggerAndResolve(this, resolve);
      } else {
        setTimeout(() => {
          setDataTriggerAndResolve(this, resolve);
        }, delayMs);
      }
    });
  }

  /**
   * Disables all observers and resets provider to its initial state.
   */
  reset(): void {
    this.registerMethods();
    this.registerObservables();

    this.states = [];
    this.stateIndex = 0;

    // This state data is more complicated so the behavior of the get/set
    // methods is a little different than other fakes in that they don't return
    // undefined by default.
    this.components = [];
    this.setGetLogResult('');
    this.setSaveLogResult({'path': ''});

    this.lastCompleteLast3pDiagnosticsInstallationApproval = null;
    this.wasShow3pDiagnosticsAppCalled = false;
    this.setGet3pDiagnosticsProviderResult(null);
    this.setAsyncOperationDelayMs(/* delayMs= */ 0);
  }

  /**
   * Setup method resolvers.
   */
  private registerMethods(): void {
    this.methods = new FakeMethodResolver();

    this.methods.register('getCurrentState');
    this.methods.register('transitionPreviousState');

    this.methods.register('abortRma');

    this.methods.register('canExit');
    this.methods.register('canGoBack');

    this.methods.register('beginFinalization');

    this.methods.register('trackConfiguredNetworks');
    this.methods.register('networkSelectionComplete');

    this.methods.register('getCurrentOsVersion');
    this.methods.register('checkForOsUpdates');
    this.methods.register('updateOs');
    this.methods.register('updateOsSkipped');

    this.methods.register('setSameOwner');
    this.methods.register('setDifferentOwner');
    this.methods.register('setWipeDevice');

    this.methods.register('chooseManuallyDisableWriteProtect');
    this.methods.register('chooseRsuDisableWriteProtect');
    this.methods.register('getRsuDisableWriteProtectChallenge');
    this.methods.register('getRsuDisableWriteProtectHwid');
    this.methods.register('getRsuDisableWriteProtectChallengeQrCode');
    this.methods.register('setRsuDisableWriteProtectCode');

    this.methods.register('writeProtectManuallyDisabled');

    this.methods.register('getWriteProtectDisableCompleteAction');
    this.methods.register('confirmManualWpDisableComplete');

    this.methods.register('shutdownForRestock');
    this.methods.register('continueFinalizationAfterRestock');

    this.methods.register('getComponentList');
    this.methods.register('setComponentList');
    this.methods.register('reworkMainboard');

    this.methods.register('roFirmwareUpdateComplete');

    this.methods.register('getRegionList');
    this.methods.register('getSkuList');
    this.methods.register('getCustomLabelList');
    this.methods.register('getSkuDescriptionList');
    this.methods.register('getOriginalSerialNumber');
    this.methods.register('getOriginalRegion');
    this.methods.register('getOriginalSku');
    this.methods.register('getOriginalCustomLabel');
    this.methods.register('getOriginalDramPartNumber');
    this.methods.register('getOriginalFeatureLevel');
    this.methods.register('setDeviceInformation');

    this.methods.register('getCalibrationComponentList');
    this.methods.register('getCalibrationSetupInstructions');
    this.methods.register('startCalibration');
    this.methods.register('runCalibrationStep');
    this.methods.register('continueCalibration');
    this.methods.register('calibrationComplete');

    this.methods.register('retryProvisioning');
    this.methods.register('provisioningComplete');

    this.methods.register('retryFinalization');
    this.methods.register('finalizationComplete');

    this.methods.register('writeProtectManuallyEnabled');

    this.methods.register('getLog');
    this.methods.register('saveLog');
    this.methods.register('getPowerwashRequired');
    this.methods.register('endRma');

    // Critical error handling
    this.methods.register('criticalErrorExitToLogin');
    this.methods.register('criticalErrorReboot');

    this.methods.register('shutDownAfterHardwareError');

    this.methods.register('get3pDiagnosticsProvider');
    this.methods.register('getInstallable3pDiagnosticsAppPath');
    this.methods.register('installLastFound3pDiagnosticsApp');
    this.methods.register('show3pDiagnosticsApp');
  }

  /**
   * Setup observables.
   */
  private registerObservables(): void {
    if (this.observables) {
      this.observables.stopAllTriggerIntervals();
    }
    this.observables = new FakeObservables();
    this.observables.register('ErrorObserver_onError');
    this.observables.register('OsUpdateObserver_onOsUpdateProgressUpdated');
    this.observables.register(
        'UpdateRoFirmwareObserver_onUpdateRoFirmwareStatusChanged');
    this.observables.register('CalibrationObserver_onCalibrationUpdated');
    this.observables.register('CalibrationObserver_onCalibrationStepComplete');
    this.observables.register('ProvisioningObserver_onProvisioningUpdated');
    this.observables.register(
        'HardwareWriteProtectionStateObserver_onHardwareWriteProtectionStateChanged');
    this.observables.register(
        'PowerCableStateObserver_onPowerCableStateChanged');
    this.observables.register(
        'ExternalDiskStateObserver_onExternalDiskStateChanged');
    this.observables.register(
        'HardwareVerificationStatusObserver_onHardwareVerificationResult');
    this.observables.register('FinalizationObserver_onFinalizationUpdated');
  }

  private getNextStateForMethod(method: string, expectedState: State):
      Promise<{stateResult: StateResult}> {
    if (this.states.length === 0) {
      this.setFakeStateForMethod(
          method, State.kUnknown, false, false, RmadErrorCode.kRmaNotRequired);
    } else if (this.stateIndex >= this.states.length - 1) {
      // It should not be possible for stateIndex to be out of range unless
      // there is a bug in the fake.
      assert(this.stateIndex < this.states.length);
      const state = this.states[this.stateIndex];
      this.setFakeStateForMethod(
          method, state.state, state.canExit, state.canGoBack,
          RmadErrorCode.kTransitionFailed);
    } else if (this.states[this.stateIndex].state !== expectedState) {
      // Error: Called in wrong state.
      const state = this.states[this.stateIndex];
      this.setFakeStateForMethod(
          method, state.state, state.canExit, state.canGoBack,
          RmadErrorCode.kRequestInvalid);
    } else {
      // Success.
      this.stateIndex++;
      const state = this.states[this.stateIndex];
      this.setFakeStateForMethod(
          method, state.state, state.canExit, state.canGoBack, state.error);
    }
    return this.methods.resolveMethodWithDelay(
        method, this.resolveMethodDelayMs);
  }

  /**
   * Sets the value that will be returned when calling getCurrent().
   */
  private setFakeCurrentState(
      state: State, canExit: boolean, canGoBack: boolean,
      error: RmadErrorCode): void {
    this.setFakeStateForMethod(
        'getCurrentState', state, canExit, canGoBack, error);
  }

  /**
   * Sets the value that will be returned when calling
   * transitionPreviousState().
   */
  private setFakePrevState(
      state: State, canExit: boolean, canGoBack: boolean,
      error: RmadErrorCode): void {
    this.setFakeStateForMethod(
        'transitionPreviousState', state, canExit, canGoBack, error);
  }

  /**
   * Sets the value that will be returned when calling state specific functions
   * that update state. e.g. setSameOwner()
   */
  private setFakeStateForMethod(
      method: string, state: State, canExit: boolean, canGoBack: boolean,
      error: RmadErrorCode): void {
    this.methods.setResult(method, {
      stateResult: {
        state: state,
        canExit: canExit,
        canGoBack: canGoBack,
        error: error,
      },
    });
  }
}