import EventEmitter from 'events';

import { assert } 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}
`;

function getFakeADBKit({ adbClient = {}, adbkitUtil = {}, adbDevice = {} }) {
  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');
    fakeADBError.code = 'ENOENT';
    return Promise.reject(fakeADBError);
  });
}

async function testSpawnADBUsageError({
  testFn,
  adbClient,
  adbkitUtil,
  adbDevice,
}) {
  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);
    });

    it('retrieves current user', async () => {
      const adb = getFakeADBKit({
        adbkitUtil: {
          readAll: sinon.spy(() => {
            return Promise.resolve(Buffer.from('123\n'));
          }),
        },
      });

      const adbUtils = new ADBUtils({ adb });
      const promise = adbUtils.getCurrentUser();

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(adb.fakeADBDevice.shell, [
        'am',
        'get-current-user',
      ]);
      const result = await assert.isFulfilled(promise);
      assert.equal(result, 123);
    });

    it('rejects invalid get-current-user output', async () => {
      const adb = getFakeADBKit({
        adbkitUtil: {
          readAll: sinon.spy(() => {
            return Promise.resolve(Buffer.from('No user'));
          }),
        },
      });

      const adbUtils = new ADBUtils({ adb });
      const promise = adbUtils.getCurrentUser();

      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(adb.fakeADBDevice.shell, [
        'am',
        'get-current-user',
      ]);
      await assert.isRejected(
        promise,
        WebExtError,
        /Unable to retrieve current user/,
      );
    });

    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 stubGetCurrentUser = sinon.stub(
        ADBUtils.prototype,
        'getCurrentUser',
      );
      stubGetCurrentUser.resolves(0);

      const adbUtils = new ADBUtils({ adb });

      const promise = adbUtils.discoverInstalledFirefoxAPKs('device1');
      const packages = await assert.isFulfilled(promise);
      sinon.assert.calledOnce(stubGetCurrentUser);
      sinon.assert.calledWith(stubGetCurrentUser, 'device1');
      sinon.assert.calledOnce(adb.fakeADBDevice.shell);
      sinon.assert.calledWith(adb.fakeADBDevice.shell, [
        'pm',
        'list',
        'packages',
        '--user',
        '0',
      ]);
      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('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, isDirectory) {
      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.calledTwice(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,
      });
      sinon.assert.calledWithMatch(adb.fakeADBDevice.startActivity, {
        action: 'android.intent.action.MAIN',
        category: 'android.intent.category.LAUNCHER',
        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,
      expectedApkComponent,
    ) {
      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 fully-qualified APK component on the build-variant: ' +
        'org.mozilla.fenix.debug',
      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.debug',
          null, // 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.debug/' + 'org.mozilla.fenix.debug.App',
          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']);
    });
  });
});
