/* @flow */

import EventEmitter from 'events';

import chai from 'chai';
import { afterEach, describe, it } from 'mocha';
import * as sinon from 'sinon';

import { UsageError, WebExtError } from '../../../src/errors.js';
import ADBUtils, {
  ARTIFACTS_DIR_PREFIX,
  DEVICE_DIR_BASE,
  listADBDevices,
  listADBFirefoxAPKs,
} from '../../../src/util/adb.js';
import {
  consoleStream, // instance is imported to inspect logged messages
} from '../../../src/util/logger.js';

const fakeADBPackageList = `
package:org.mozilla.fennec
package:org.mozilla.firefox
package:com.some.firefox.fork
package:com.some.firefox.fork2
package:org.some.other.software
`;

// NOTE: fake /proc/net/unix output format based on the output collected from
// an android system.
const fakeSocketFilePrefix =
  '00000000: 00000003 00000000 00000000 0001 03  1857';

const fakeRDPUnixSocketFile =
  '/data/data/org.mozilla.firefox/firefox-debugger-socket';

const fakeUnixSocketFiles = `
${fakeSocketFilePrefix} /dev/socket/mdns
${fakeSocketFilePrefix}  ${fakeRDPUnixSocketFile}
`;

// NOTE: fake 'pm dump <APK>' output related to the granted permissions for an
// android application.
const fakeAndroidGrantedPermissionsColon = `
android.permission.READ_EXTERNAL_STORAGE: granted=true
android.permission.WRITE_EXTERNAL_STORAGE: granted=true
`;

// Some Android device uses a comma instead of a double colon
// (See #1583).
const fakeAndroidGrantedPermissionsComma = `
android.permission.READ_EXTERNAL_STORAGE, granted=true
android.permission.WRITE_EXTERNAL_STORAGE, granted=true
`;

const { assert } = chai;

function getFakeADBKit({
  adbClient = {},
  adbkitUtil = {},
  adbDevice = {},
}: {
  adbClient?: Object,
  adbkitUtil?: Object,
  adbDevice?: Object,
}) {
  const fakeTransfer = new EventEmitter();
  const adbUtilReadAllStub = sinon.stub();

  adbUtilReadAllStub.onCall(0).returns(Promise.resolve(Buffer.from('')));

  const fakeADBDevice = {
    forward: sinon.spy(() => {}),
    readdir: sinon.spy(() => Promise.resolve([])),
    shell: sinon.spy(() => Promise.resolve('')),
    startActivity: sinon.spy(() => {}),
    ...adbDevice,
  };

  const fakeADBClient = {
    listDevices: sinon.spy(() => {
      return [];
    }),
    getDevice: sinon.spy(() => fakeADBDevice),
    ...adbClient,
  };

  return {
    fakeADBClient,
    fakeADBDevice,
    fakeTransfer,
    createClient: sinon.spy(() => {
      return fakeADBClient;
    }),
    util: {
      readAll: adbUtilReadAllStub,
      ...adbkitUtil,
    },
  };
}

function createSpawnADBErrorSpy() {
  return sinon.spy(() => {
    const fakeADBError = new Error('spawn adb');
    // $FlowFixMe: reuse ErrorWithCode from other tests
    fakeADBError.code = 'ENOENT';
    return Promise.reject(fakeADBError);
  });
}

async function testSpawnADBUsageError({
  testFn,
  adbClient,
  adbkitUtil,
  adbDevice,
}: {
  testFn: Function,
  adbClient?: Object,
  adbkitUtil?: Object,
  adbDevice?: Object,
}) {
  const adb = getFakeADBKit({ adbClient, adbkitUtil, adbDevice });
  const adbUtils = new ADBUtils({ adb });

  const promise = testFn(adbUtils);

  await assert.isRejected(promise, UsageError);
  await assert.isRejected(promise, /No adb executable has been found/);

  // Return the adb object to allow further assertion on the sinon spies.
  return adb;
}

describe('utils/adb', () => {
  describe('discoverDevices', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbClient: {
          listDevices: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) => adbUtils.discoverDevices(),
      });

      sinon.assert.calledOnce(adb.fakeADBClient.listDevices);
    });

    it('resolves the array of the android device ids', async () => {
      const adb = getFakeADBKit({
        adbClient: {
          listDevices: sinon.spy(() => [
            { id: 'emulator1' },
            { id: 'device2' },
          ]),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.discoverDevices();

      const devices = await assert.isFulfilled(promise);
      sinon.assert.calledOnce(adb.fakeADBClient.listDevices);
      assert.deepEqual(devices, ['emulator1', 'device2']);
    });
  });

  describe('runShellCommand', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbDevice: {
          shell: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) =>
          adbUtils.runShellCommand('device1', 'test -d /some/dir && echo 1'),
      });

      sinon.assert.calledOnce(adb.fakeADBClient.getDevice);
      sinon.assert.calledWith(adb.fakeADBClient.getDevice, 'device1');
      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(
        adb.fakeADBDevice.shell,
        'test -d /some/dir && echo 1'
      );
    });

    it('rejects on any unexpected exception', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => {
            throw new Error('Unexpected error');
          }),
        },
      });

      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.runShellCommand(
        'device1',
        'test -d /some/dir && echo 1'
      );

      await assert.isRejected(promise, /Unexpected error/);

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(
        adb.fakeADBDevice.shell,
        'test -d /some/dir && echo 1'
      );
    });

    it('resolves the shell command output as a string', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => {
            return Promise.resolve(Buffer.from('fake_data_result'));
          }),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.runShellCommand(
        'device1',
        'echo fake_data_result'
      );
      const result = await assert.isFulfilled(promise);
      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledOnce(adb.util.readAll);
      assert.equal(result, 'fake_data_result');
    });
  });

  describe('discoverInstalledFirefoxAPKs', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbDevice: {
          shell: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) => adbUtils.discoverInstalledFirefoxAPKs('device1'),
      });

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(adb.fakeADBDevice.shell, [
        'pm',
        'list',
        'packages',
      ]);
    });

    it('resolves the array of the installed firefox APKs', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => {
            return Promise.resolve(Buffer.from(fakeADBPackageList));
          }),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.discoverInstalledFirefoxAPKs('device1');
      const packages = await assert.isFulfilled(promise);
      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledOnce(adb.util.readAll);
      assert.deepEqual(packages, ['org.mozilla.fennec', 'org.mozilla.firefox']);
    });

    it('resolves the given firefox APK with exact package name', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => {
            return Promise.resolve(Buffer.from(fakeADBPackageList));
          }),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.discoverInstalledFirefoxAPKs(
        'device1',
        'com.some.firefox.fork'
      );
      const packages = await assert.isFulfilled(promise);
      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledOnce(adb.util.readAll);
      assert.deepEqual(packages, ['com.some.firefox.fork']);
    });
  });

  describe('getAndroidVersionNumber', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbDevice: {
          shell: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) => adbUtils.getAndroidVersionNumber('device1'),
      });

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(adb.fakeADBDevice.shell, [
        'getprop',
        'ro.build.version.sdk',
      ]);
    });

    it('rejects a WebExtError when unable to return a number', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => {
            return Promise.resolve(Buffer.from('UnexpectedNaN'));
          }),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.getAndroidVersionNumber('device1');

      await assert.isRejected(promise, WebExtError);
      await assert.isRejected(
        promise,
        'Unable to discovery android version on device1: UnexpectedNaN'
      );

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(adb.fakeADBDevice.shell, [
        'getprop',
        'ro.build.version.sdk',
      ]);
    });

    it('resolves the android version number', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => {
            return Promise.resolve(Buffer.from('21'));
          }),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.getAndroidVersionNumber('device1');

      const versionNumber = await assert.isFulfilled(promise);
      assert.equal(versionNumber, 21);

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(adb.fakeADBDevice.shell, [
        'getprop',
        'ro.build.version.sdk',
      ]);
    });
  });

  describe('ensureRequiredAPKRuntimePermissions', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbDevice: {
          shell: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) =>
          adbUtils.ensureRequiredAPKRuntimePermissions(
            'device1',
            'org.mozilla.firefox',
            [
              'android.permission.READ_EXTERNAL_STORAGE',
              'android.permission.WRITE_EXTERNAL_STORAGE',
            ]
          ),
      });

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(adb.fakeADBDevice.shell, [
        'pm',
        'dump',
        'org.mozilla.firefox',
      ]);
    });

    it('rejects an UsageError when a required permission has not been granted', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => {
            // No granted permissions in the output.
            return Promise.resolve(Buffer.from(''));
          }),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const permissions = [
        'android.permission.READ_EXTERNAL_STORAGE',
        'android.permission.WRITE_EXTERNAL_STORAGE',
      ];
      const promise = adbUtils.ensureRequiredAPKRuntimePermissions(
        'device1',
        'org.mozilla.firefox',
        permissions
      );

      await assert.isRejected(promise, UsageError);
      await assert.isRejected(
        promise,
        new RegExp(`Required ${permissions[0]} has not be granted`)
      );

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(adb.fakeADBDevice.shell, [
        'pm',
        'dump',
        'org.mozilla.firefox',
      ]);
    });

    describe('Android Granted Permissions discovery', async () => {
      async function assertPermissions(fakeAndroidPmDump) {
        const adb = getFakeADBKit({
          adbDevice: {
            shell: sinon.spy(() => Promise.resolve('')),
          },
          adbkitUtil: {
            readAll: sinon.spy(() => {
              return Promise.resolve(Buffer.from(fakeAndroidPmDump));
            }),
          },
        });
        const adbUtils = new ADBUtils({ adb });

        const promise = adbUtils.ensureRequiredAPKRuntimePermissions(
          'device1',
          'org.mozilla.firefox',
          [
            'android.permission.READ_EXTERNAL_STORAGE',
            'android.permission.WRITE_EXTERNAL_STORAGE',
          ]
        );

        await assert.isFulfilled(promise);
        sinon.assert.calledOnce(adb.fakeADBDevice.shell);
        sinon.assert.calledWith(adb.fakeADBDevice.shell, [
          'pm',
          'dump',
          'org.mozilla.firefox',
        ]);
      }

      it('detects permissions on pm versions that uses colon separator', () =>
        assertPermissions(fakeAndroidGrantedPermissionsColon));

      it('detects permissions on pm versions that uses comma separator', () =>
        assertPermissions(fakeAndroidGrantedPermissionsComma));
    });
  });

  describe('amForceStopAPK', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbDevice: {
          shell: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) =>
          adbUtils.amForceStopAPK('device1', 'org.mozilla.firefox'),
      });

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(adb.fakeADBDevice.shell, [
        'am',
        'force-stop',
        'org.mozilla.firefox',
      ]);
    });

    it('does not reject when "am force-stop" has been called successfully', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.amForceStopAPK('device1', 'org.mozilla.firefox');

      await assert.isFulfilled(promise);
      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(adb.fakeADBDevice.shell, [
        'am',
        'force-stop',
        'org.mozilla.firefox',
      ]);
    });
  });

  describe('getOrCreateArtifactsDir', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbDevice: {
          shell: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) => adbUtils.getOrCreateArtifactsDir('device1'),
      });

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWithMatch(
        adb.fakeADBDevice.shell,
        /test -d (.*) ; echo \$\?/
      );
    });

    it('rejects a WebExtError if the artifact dir path exists', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => Promise.resolve(Buffer.from('0\n'))),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.getOrCreateArtifactsDir('device1');

      await assert.isRejected(promise, WebExtError);
      await assert.isRejected(
        promise,
        /Cannot create artifacts directory (.*) because it exists on (.*)/
      );

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWithMatch(
        adb.fakeADBDevice.shell,
        /test -d (.*) ; echo \$\?/
      );
    });

    it('resolves to the android artifacts dir path', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => Promise.resolve(Buffer.from('1\n'))),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.getOrCreateArtifactsDir('device1');

      const result = await assert.isFulfilled(promise);

      assert.match(result, /^\/data\/local\/tmp\/web-ext-artifacts-/);

      sinon.assert.calledTwice(adb.fakeADBDevice.shell);
      sinon.assert.calledWithMatch(
        adb.fakeADBDevice.shell,
        `test -d ${result} ; echo $?`
      );
      sinon.assert.calledWithMatch(adb.fakeADBDevice.shell, [
        'mkdir',
        '-p',
        result,
      ]);
    });

    it('does not create a new artifact dir if it has been already created', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => Promise.resolve(Buffer.from('1\n'))),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      // Add an artifact dir to the adbUtils internal map.
      const fakeArtifactsDir =
        '/data/local/tmp/web-ext-artifacts-already-created';
      adbUtils.artifactsDirMap.set('device1', fakeArtifactsDir);

      const promise = adbUtils.getOrCreateArtifactsDir('device1');

      const result = await assert.isFulfilled(promise);
      assert.equal(result, fakeArtifactsDir);

      sinon.assert.notCalled(adb.fakeADBDevice.shell);
    });
  });

  describe('clearArtifactsDir', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbDevice: {
          shell: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) => {
          adbUtils.artifactsDirMap.set(
            'device1',
            '/data/local/tmp/webext-artifacts-fake'
          );
          return adbUtils.clearArtifactsDir('device1');
        },
      });

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.shell, [
        'rm',
        '-rf',
        '/data/local/tmp/webext-artifacts-fake',
      ]);
    });

    it('removes the directory if it has been previously created', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => Promise.resolve(Buffer.from('\n'))),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      adbUtils.artifactsDirMap.set(
        'device1',
        '/data/local/tmp/webext-artifacts-fake'
      );
      const promise = adbUtils.clearArtifactsDir('device1');

      await assert.isFulfilled(promise);

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.shell, [
        'rm',
        '-rf',
        '/data/local/tmp/webext-artifacts-fake',
      ]);
    });

    it('is a no-op if no artifacts dir has been previously created', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.clearArtifactsDir('device1');

      await assert.isFulfilled(promise);

      sinon.assert.notCalled(adb.fakeADBDevice.shell);
    });
  });

  describe('detectOrRemoveOldArtifacts', () => {
    function createFakeReaddirFile(artifactName: string, isDirectory: boolean) {
      return {
        name: artifactName,
        isDirectory: () => {
          return isDirectory;
        },
      };
    }

    const filesNotArtifactsDirs = [
      createFakeReaddirFile('not-an-artifact-dir1', true),
      createFakeReaddirFile('not-a-dir2', false),
    ];

    const filesArtifactsDirs = [
      createFakeReaddirFile(`${ARTIFACTS_DIR_PREFIX}1`, true),
      createFakeReaddirFile(`${ARTIFACTS_DIR_PREFIX}2`, true),
    ];

    const allFiles = [...filesNotArtifactsDirs, ...filesArtifactsDirs];

    const sb = sinon.createSandbox();
    const adbkitSpies = {
      adbDevice: {
        readdir: sb.spy(() => Promise.resolve([])),
        shell: sb.spy(() => Promise.resolve('')),
      },
      adbkitUtil: {
        readAll: sb.spy(() => Promise.resolve(Buffer.from('1\n'))),
      },
    };

    // Reset the fakeADBClient spies after each test case.
    afterEach(() => sb.reset());

    it('detects old artifacts directories', async () => {
      const adb = getFakeADBKit(adbkitSpies);
      const adbUtils = new ADBUtils({ adb });
      const fakeADB = adb.fakeADBDevice;

      fakeADB.readdir = sb.spy(async () => filesNotArtifactsDirs);

      await assert.becomes(
        adbUtils.detectOrRemoveOldArtifacts('device1', false),
        false,
        'Expected to return false when no old artifacts dirs have been found'
      );
      sinon.assert.calledOnce(fakeADB.readdir);
      sinon.assert.calledWith(fakeADB.readdir, DEVICE_DIR_BASE);
      // Expect adbkit shell to never be called when no artifacts have been found.
      sinon.assert.notCalled(fakeADB.shell);

      adb.fakeADBDevice.readdir = sb.spy(async () => allFiles);

      await assert.becomes(
        adbUtils.detectOrRemoveOldArtifacts('device1', false),
        true,
        'Expected to return true when old artifacts dirs have been found'
      );
      sinon.assert.notCalled(fakeADB.shell);
    });

    it('does optionally remove artifacts directories', async () => {
      const adb = getFakeADBKit(adbkitSpies);
      const adbUtils = new ADBUtils({ adb });

      adb.fakeADBDevice.readdir = sb.spy(async () => allFiles);

      await assert.becomes(
        adbUtils.detectOrRemoveOldArtifacts('device1', true),
        true,
        'Expected to return true when old artifacts dirs have been found'
      );

      sinon.assert.calledOnce(adb.fakeADBDevice.readdir);
      assert.equal(
        adb.fakeADBDevice.shell.callCount,
        filesArtifactsDirs.length
      );

      for (const fakeFile of filesArtifactsDirs) {
        sinon.assert.calledWithMatch(adb.fakeADBDevice.shell, [
          'rm',
          '-rf',
          `${DEVICE_DIR_BASE}${fakeFile.name}`,
        ]);
      }
    });
  });

  describe('pushFile', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbDevice: {
          push: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) =>
          adbUtils.pushFile('device1', '/fake/src', '/fake/dest'),
      });

      sinon.assert.calledOnce(adb.fakeADBDevice.push);
      sinon.assert.calledWithMatch(
        adb.fakeADBDevice.push,
        '/fake/src',
        '/fake/dest'
      );
    });

    it('resolves when the file has been completely transfered', async () => {
      const fakeTransfer = new EventEmitter();
      const fakeTransferPromise = Promise.resolve(fakeTransfer);
      const adb = getFakeADBKit({
        adbDevice: {
          push: sinon.spy(() => fakeTransferPromise),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.pushFile(
        'device1',
        '/fake/local/path',
        '/fake/remote/path'
      );

      await fakeTransferPromise;
      fakeTransfer.emit('end');

      await assert.isFulfilled(promise);

      sinon.assert.calledOnce(adb.fakeADBDevice.push);
      sinon.assert.calledWithMatch(
        adb.fakeADBDevice.push,
        '/fake/local/path',
        'fake/remote/path'
      );
    });
  });

  describe('startFirefoxAPK', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbDevice: {
          startActivity: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) => {
          return adbUtils.startFirefoxAPK(
            'device1',
            'org.mozilla.firefox_mybuild',
            undefined, // firefoxApkComponent
            '/fake/custom/profile/path'
          );
        },
      });

      sinon.assert.calledOnce(adb.fakeADBDevice.startActivity);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.startActivity, {
        action: 'android.activity.MAIN',
        component: 'org.mozilla.firefox_mybuild/.App',
        extras: [
          {
            key: 'args',
            value: '-profile /fake/custom/profile/path',
          },
        ],
        wait: true,
      });
    });

    it('starts Firefox APK on a custom profile (only used by Fennec)', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          startActivity: sinon.spy(() => Promise.resolve()),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => Promise.resolve(Buffer.from('\n'))),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promiseCompatibilityMode = adbUtils.startFirefoxAPK(
        'device1',
        'org.mozilla.firefox_mybuild',
        undefined, // firefoxApkComponent
        '/fake/custom/profile/path'
      );

      await assert.isFulfilled(promiseCompatibilityMode);

      const expectedAdbParams = {
        action: 'android.activity.MAIN',
        component: 'org.mozilla.firefox_mybuild/.App',
        extras: [
          {
            key: 'args',
            value: '-profile /fake/custom/profile/path',
          },
        ],
        wait: true,
      };

      sinon.assert.calledOnce(adb.fakeADBDevice.startActivity);
      sinon.assert.calledWithMatch(
        adb.fakeADBDevice.startActivity,
        expectedAdbParams
      );
    });

    it('starts a given APK component without a period', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          startActivity: sinon.spy(() => Promise.resolve()),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => Promise.resolve(Buffer.from('\n'))),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.startFirefoxAPK(
        'device1',
        'org.mozilla.geckoview_example',
        'GeckoViewActivity', // firefoxApkComponent
        '/fake/custom/profile/path'
      );

      await assert.isFulfilled(promise);

      sinon.assert.calledOnce(adb.fakeADBDevice.startActivity);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.startActivity, {
        action: 'android.activity.MAIN',
        component:
          'org.mozilla.geckoview_example' +
          '/org.mozilla.geckoview_example.GeckoViewActivity',
        extras: [
          {
            key: 'args',
            value: '-profile /fake/custom/profile/path',
          },
        ],
        wait: true,
      });
    });

    it('starts a given APK component with a period', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          startActivity: sinon.spy(() => Promise.resolve()),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => Promise.resolve(Buffer.from('\n'))),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.startFirefoxAPK(
        'device1',
        'org.mozilla.geckoview_example',
        'org.mozilla.geckoview_example.GeckoViewActivity', // firefoxApkComponent
        '/fake/custom/profile/path'
      );

      await assert.isFulfilled(promise);

      sinon.assert.calledOnce(adb.fakeADBDevice.startActivity);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.startActivity, {
        action: 'android.activity.MAIN',
        component:
          'org.mozilla.geckoview_example/' +
          'org.mozilla.geckoview_example.GeckoViewActivity',
        extras: [
          {
            key: 'args',
            value: '-profile /fake/custom/profile/path',
          },
        ],
        wait: true,
      });
    });

    it('starts a given APK component on fenix.nightly', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          startActivity: sinon.spy(() => Promise.resolve()),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => Promise.resolve(Buffer.from('\n'))),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.startFirefoxAPK(
        'device1',
        'org.mozilla.fenix.nightly',
        'HomeActivity', // firefoxApkComponent
        '/fake/custom/profile/path'
      );

      await assert.isFulfilled(promise);

      sinon.assert.calledOnce(adb.fakeADBDevice.startActivity);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.startActivity, {
        action: 'android.activity.MAIN',
        component:
          'org.mozilla.fenix.nightly/' + 'org.mozilla.fenix.HomeActivity',
        extras: [
          {
            key: 'args',
            value: '-profile /fake/custom/profile/path',
          },
        ],
        wait: true,
      });
    });

    async function testReferenceBrowserApkComponent(
      firefoxApkComponent?: string,
      expectedApkComponent: string
    ) {
      const adb = getFakeADBKit({
        adbDevice: {
          startActivity: sinon.spy(() => Promise.resolve()),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => Promise.resolve(Buffer.from('\n'))),
        },
      });
      const adbUtils = new ADBUtils({ adb });
      const apkName = 'org.mozilla.reference.browser';
      const component = `${apkName}/${apkName}.${expectedApkComponent}`;
      const promise = adbUtils.startFirefoxAPK(
        'device1',
        apkName,
        firefoxApkComponent,
        '/fake/custom/profile/path'
      );

      await assert.isFulfilled(promise);
      sinon.assert.calledOnce(adb.fakeADBDevice.startActivity);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.startActivity, {
        action: 'android.activity.MAIN',
        component,
        extras: [
          {
            key: 'args',
            value: '-profile /fake/custom/profile/path',
          },
        ],
        wait: true,
      });
    }

    it('start reference browser without APK component', () => {
      return testReferenceBrowserApkComponent(undefined, 'BrowserActivity');
    });

    it('start reference browser with custom APK component', () => {
      return testReferenceBrowserApkComponent(
        'CustomActivity',
        'CustomActivity'
      );
    });

    it('starts without specifying an APK component', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          startActivity: sinon.spy(() => Promise.resolve()),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => Promise.resolve(Buffer.from('\n'))),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.startFirefoxAPK(
        'device1',
        'org.mozilla.geckoview_example',
        undefined, // firefoxApkComponent
        '/fake/custom/profile/path'
      );

      await assert.isFulfilled(promise);

      sinon.assert.calledOnce(adb.fakeADBDevice.startActivity);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.startActivity, {
        action: 'android.activity.MAIN',
        component:
          'org.mozilla.geckoview_example/' +
          'org.mozilla.geckoview_example.App',
        extras: [
          {
            key: 'args',
            value: '-profile /fake/custom/profile/path',
          },
        ],
        wait: true,
      });
    });

    it(
      'starts a fully-qualified APK component on the build-variant: ' +
        'fenix.nightly',
      async () => {
        const adb = getFakeADBKit({
          adbDevice: {
            startActivity: sinon.spy(() => Promise.resolve()),
          },
          adbkitUtil: {
            readAll: sinon.spy(() => Promise.resolve(Buffer.from('\n'))),
          },
        });
        const adbUtils = new ADBUtils({ adb });

        const promise = adbUtils.startFirefoxAPK(
          'device1',
          'org.mozilla.fenix.nightly',
          'org.mozilla.fenix.HomeActivity', // firefoxApkComponent
          '/fake/custom/profile/path'
        );

        await assert.isFulfilled(promise);

        sinon.assert.calledOnce(adb.fakeADBDevice.startActivity);
        sinon.assert.calledWithMatch(adb.fakeADBDevice.startActivity, {
          action: 'android.activity.MAIN',
          component:
            'org.mozilla.fenix.nightly/' + 'org.mozilla.fenix.HomeActivity',
          extras: [
            {
              key: 'args',
              value: '-profile /fake/custom/profile/path',
            },
          ],
          wait: true,
        });
      }
    );

    it('starts a given APK component that begins with a period', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          startActivity: sinon.spy(() => Promise.resolve()),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => Promise.resolve(Buffer.from('\n'))),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.startFirefoxAPK(
        'device1',
        'org.mozilla.fenix.nightly',
        '.HomeActivity', // firefoxApkComponent
        '/fake/custom/profile/path'
      );

      await assert.isFulfilled(promise);

      sinon.assert.calledOnce(adb.fakeADBDevice.startActivity);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.startActivity, {
        action: 'android.activity.MAIN',
        component:
          'org.mozilla.fenix.nightly/' + 'org.mozilla.fenix.HomeActivity',
        extras: [
          {
            key: 'args',
            value: '-profile /fake/custom/profile/path',
          },
        ],
        wait: true,
      });
    });
  });

  describe('discoverRDPUnixSocket', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbDevice: {
          shell: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) => {
          return adbUtils.discoverRDPUnixSocket(
            'device1',
            'org.mozilla.firefox_mybuild'
          );
        },
      });

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.shell, [
        'cat',
        '/proc/net/unix',
      ]);
    });

    it('rejects an UsageError on setUserAbortDiscovery call', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => {
            return Promise.resolve(Buffer.from(''));
          }),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.discoverRDPUnixSocket(
        'device1',
        'org.mozilla.firefox_mybuild'
      );

      adbUtils.setUserAbortDiscovery(true);

      await assert.isRejected(promise, UsageError);
      await assert.isRejected(
        promise,
        'Exiting Firefox Remote Debugging socket discovery on user request'
      );

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.shell, [
        'cat',
        '/proc/net/unix',
      ]);
    });

    it('rejects a WebExtError on timeouts', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => {
            return Promise.resolve(Buffer.from(''));
          }),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const maxDiscoveryTime = 50;
      const retryInterval = 10;

      const promise = adbUtils.discoverRDPUnixSocket(
        'device1',
        'org.mozilla.firefox_mybuild',
        {
          maxDiscoveryTime,
          retryInterval,
        }
      );

      await assert.isRejected(promise, WebExtError);
      await assert.isRejected(
        promise,
        'Timeout while waiting for the Android Firefox Debugger Socket'
      );

      sinon.assert.called(adb.fakeADBDevice.shell);
      sinon.assert.alwaysCalledWithMatch(adb.fakeADBDevice.shell, [
        'cat',
        '/proc/net/unix',
      ]);
    });

    it('reminds the user to enable remote_debugging', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve('')),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => {
            return Promise.resolve(Buffer.from(''));
          }),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      consoleStream.flushCapturedLogs();
      consoleStream.makeVerbose();
      consoleStream.startCapturing();

      const promise = adbUtils.discoverRDPUnixSocket(
        'device1',
        'org.mozilla.firefox_mybuild',
        {
          maxDiscoveryTime: 50,
          retryInterval: 10,
        }
      );
      await assert.isRejected(promise, WebExtError);

      const { capturedMessages } = consoleStream;
      const foundMessage = capturedMessages.find((message) =>
        message.includes('Make sure to enable "Remote Debugging via USB')
      );

      consoleStream.stopCapturing();

      assert.ok(foundMessage);
      assert.ok(foundMessage && foundMessage.includes('[info]'));
    });

    it('rejects a WebExtError if more than one RDP socket have been found', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve()),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => {
            // Fake unexpected multiple RDP socket matches.
            return Promise.resolve(
              Buffer.from(`${fakeUnixSocketFiles}${fakeUnixSocketFiles}`)
            );
          }),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.discoverRDPUnixSocket(
        'device1',
        'org.mozilla.firefox'
      );

      await assert.isRejected(promise, WebExtError);
      await assert.isRejected(promise, /Unexpected multiple RDP sockets/);

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWithMatch(adb.fakeADBDevice.shell, [
        'cat',
        '/proc/net/unix',
      ]);
    });

    it('resolves the android RDP unix socket path', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve()),
        },
        adbkitUtil: {
          readAll: sinon.spy(() => {
            // Fake unexpected multiple RDP socket matches.
            return Promise.resolve(Buffer.from(fakeUnixSocketFiles));
          }),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.discoverRDPUnixSocket(
        'device1',
        'org.mozilla.firefox'
      );

      const result = await assert.isFulfilled(promise);

      assert.equal(result, fakeRDPUnixSocketFile);
    });
  });

  describe('setupForward', () => {
    it('rejects an UsageError on adb binary not found', async () => {
      const adb = await testSpawnADBUsageError({
        adbDevice: {
          forward: createSpawnADBErrorSpy(),
        },
        testFn: (adbUtils) => {
          return adbUtils.setupForward('device1', 'remote:fake', 'local:fake');
        },
      });

      sinon.assert.calledOnce(adb.fakeADBDevice.forward);
      sinon.assert.calledWithMatch(
        adb.fakeADBDevice.forward,
        'local:fake',
        'remote:fake'
      );
    });

    it('configures an adb forwarding for a given device', async () => {
      const adb = getFakeADBKit({
        adbDevice: {
          shell: sinon.spy(() => Promise.resolve()),
        },
      });
      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.setupForward(
        'device1',
        'remote:fake',
        'local:fake'
      );

      await assert.isFulfilled(promise);

      sinon.assert.calledOnce(adb.fakeADBDevice.forward);
      sinon.assert.calledWithMatch(
        adb.fakeADBDevice.forward,
        'local:fake',
        'remote:fake'
      );
    });
  });

  describe('exports exposed in util.adb', () => {
    it('should export a listADBDevices method', async () => {
      const stubDiscoverDevices = sinon.stub(
        ADBUtils.prototype,
        'discoverDevices'
      );
      stubDiscoverDevices.resolves(['emulator1', 'device2']);
      const promise = listADBDevices();
      const devices = await assert.isFulfilled(promise);
      assert.deepEqual(devices, ['emulator1', 'device2']);
    });

    it('should export a listADBFirefoxAPKs method', async () => {
      const stubDiscoverInstalledFirefoxAPKs = sinon.stub(
        ADBUtils.prototype,
        'discoverInstalledFirefoxAPKs'
      );
      stubDiscoverInstalledFirefoxAPKs.resolves([
        'package1',
        'package2',
        'package3',
      ]);
      const promise = listADBFirefoxAPKs('device1');
      const packages = await assert.isFulfilled(promise);
      sinon.assert.calledWith(stubDiscoverInstalledFirefoxAPKs, 'device1');
      assert.deepEqual(packages, ['package1', 'package2', 'package3']);
    });
  });
});
