/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 */

import {State} from '../../reducers/index';
import configureStore from 'redux-mock-store';
import {default as BaseDevice} from '../../devices/BaseDevice';
import {default as ArchivedDevice} from '../../devices/ArchivedDevice';
import {
  processStore,
  determinePluginsToProcess,
  exportStore,
  importDataToStore,
} from '../exportData';
import {FlipperPlugin, FlipperDevicePlugin} from '../../plugin';
import {default as Client, ClientExport} from '../../Client';
import {selectedPlugins, State as PluginsState} from '../../reducers/plugins';
import {createMockFlipperWithPlugin} from '../../test-utils/createMockFlipperWithPlugin';
import {
  Notification,
  TestUtils,
  _SandyPluginDefinition,
  createState,
  PluginClient,
  DevicePluginClient,
  sleep,
} from 'flipper-plugin';
import {selectPlugin} from '../../reducers/connections';
import {TestIdler} from '../Idler';

const testIdler = new TestIdler();

function testOnStatusMessage() {
  // emtpy stub
}

class TestPlugin extends FlipperPlugin<any, any, any> {}
TestPlugin.title = 'TestPlugin';
TestPlugin.id = 'TestPlugin';
class TestDevicePlugin extends FlipperDevicePlugin<any, any, any> {}
TestDevicePlugin.title = 'TestDevicePlugin';
TestDevicePlugin.id = 'TestDevicePlugin';
const logger = {
  track: () => {},
  info: () => {},
  warn: () => {},
  error: () => {},
  debug: () => {},
  trackTimeSince: () => {},
};
const mockStore = configureStore<State, {}>([])();

function generateNotifications(
  id: string,
  title: string,
  message: string,
  severity: 'warning' | 'error',
): Notification {
  return {id, title, message, severity};
}

function generateClientIdentifier(device: BaseDevice, app: string): string {
  const {os, deviceType, serial} = device;
  const identifier = `${app}#${os}#${deviceType}#${serial}`;
  return identifier;
}

function generateClientIdentifierWithSalt(
  identifier: string,
  salt: string,
): string {
  const array = identifier.split('#');
  const serial = array.pop();
  return array.join('#') + '#' + salt + '-' + serial;
}

function generateClientFromClientWithSalt(
  client: ClientExport,
  salt: string,
): ClientExport {
  const {os, device, device_id, app} = client.query;
  const identifier = generateClientIdentifierWithSalt(client.id, salt);
  return {
    id: identifier,
    query: {app, os, device, device_id: salt + '-' + device_id},
  };
}
function generateClientFromDevice(
  device: BaseDevice,
  app: string,
): ClientExport {
  const {os, deviceType, serial} = device;
  const identifier = generateClientIdentifier(device, app);
  return {
    id: identifier,
    query: {app, os, device: deviceType, device_id: serial},
  };
}

test('test generateClientIndentifierWithSalt helper function', () => {
  const device = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const identifier = generateClientIdentifier(device, 'app');
  const saltIdentifier = generateClientIdentifierWithSalt(identifier, 'salt');
  expect(saltIdentifier).toEqual('app#iOS#emulator#salt-serial');
  expect(identifier).toEqual('app#iOS#emulator#serial');
});

test('test generateClientFromClientWithSalt helper function', () => {
  const device = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const client = generateClientFromDevice(device, 'app');
  const saltedClient = generateClientFromClientWithSalt(client, 'salt');
  expect(saltedClient).toEqual({
    id: 'app#iOS#emulator#salt-serial',
    query: {
      app: 'app',
      os: 'iOS',
      device: 'emulator',
      device_id: 'salt-serial',
    },
  });
  expect(client).toEqual({
    id: 'app#iOS#emulator#serial',
    query: {
      app: 'app',
      os: 'iOS',
      device: 'emulator',
      device_id: 'serial',
    },
  });
});

test('test generateClientFromDevice helper function', () => {
  const device = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const client = generateClientFromDevice(device, 'app');
  expect(client).toEqual({
    id: 'app#iOS#emulator#serial',
    query: {
      app: 'app',
      os: 'iOS',
      device: 'emulator',
      device_id: 'serial',
    },
  });
});

test('test generateClientIdentifier helper function', () => {
  const device = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const identifier = generateClientIdentifier(device, 'app');
  expect(identifier).toEqual('app#iOS#emulator#serial');
});

test('test generateNotifications helper function', () => {
  const notification = generateNotifications('id', 'title', 'msg', 'error');
  expect(notification).toEqual({
    id: 'id',
    title: 'title',
    message: 'msg',
    severity: 'error',
  });
});

test('test processStore function for empty state', async () => {
  expect(
    processStore({
      activeNotifications: [],
      device: null,
      pluginStates: {},
      clients: [],
      devicePlugins: new Map(),
      clientPlugins: new Map(),
      salt: 'salt',
      selectedPlugins: [],
      pluginStates2: {},
    }),
  ).rejects.toMatchInlineSnapshot(
    `[Error: Selected device is null, please select a device]`,
  );
});

test('test processStore function for an iOS device connected', async () => {
  const json = await processStore({
    activeNotifications: [],
    device: new ArchivedDevice({
      serial: 'serial',
      deviceType: 'emulator',
      title: 'TestiPhone',
      os: 'iOS',
      screenshotHandle: null,
    }),
    pluginStates: {},
    pluginStates2: {},
    clients: [],
    devicePlugins: new Map(),
    clientPlugins: new Map(),
    salt: 'salt',
    selectedPlugins: [],
  });
  if (!json) {
    fail('json is undefined');
  }
  const {device, clients} = json;
  expect(json.device).toBeDefined();
  expect(clients).toEqual([]);
  if (!device) {
    fail('device is undefined');
  }
  const {serial, deviceType, title, os} = device;
  expect(serial).toEqual('salt-serial');
  expect(deviceType).toEqual('emulator');
  expect(title).toEqual('TestiPhone');
  expect(os).toEqual('iOS');
  const {pluginStates, activeNotifications} = json.store;
  expect(pluginStates).toEqual({});
  expect(activeNotifications).toEqual([]);
});

test('test processStore function for an iOS device connected with client plugin data', async () => {
  const device = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const client = generateClientFromDevice(device, 'testapp');
  const clientIdentifier = generateClientIdentifier(device, 'testapp');
  const json = await processStore({
    activeNotifications: [],
    device,
    pluginStates: {
      [`${clientIdentifier}#TestPlugin`]: {msg: 'Test plugin'},
    },
    pluginStates2: {
      [`${clientIdentifier}`]: {TestPlugin2: [{msg: 'Test plugin2'}]},
    },
    clients: [client],
    devicePlugins: new Map(),
    clientPlugins: new Map([['TestPlugin', TestPlugin]]),
    salt: 'salt',
    selectedPlugins: [],
  });
  if (!json) {
    fail('json is undefined');
  }
  const {pluginStates} = json.store;
  const expectedPluginState = {
    [`${generateClientIdentifierWithSalt(
      clientIdentifier,
      'salt',
    )}#TestPlugin`]: JSON.stringify({
      msg: 'Test plugin',
    }),
  };
  const expectedPluginState2 = {
    [`${generateClientIdentifierWithSalt(clientIdentifier, 'salt')}`]: {
      TestPlugin2: [
        {
          msg: 'Test plugin2',
        },
      ],
    },
  };
  expect(pluginStates).toEqual(expectedPluginState);
  expect(json.pluginStates2).toEqual(expectedPluginState2);
});

test('test processStore function to have only the client for the selected device', async () => {
  const selectedDevice = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const unselectedDevice = new ArchivedDevice({
    serial: 'identifier',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });

  const unselectedDeviceClientIdentifier = generateClientIdentifier(
    unselectedDevice,
    'testapp',
  );
  const selectedDeviceClientIdentifier = generateClientIdentifier(
    selectedDevice,
    'testapp',
  );
  const selectedDeviceClient = generateClientFromDevice(
    selectedDevice,
    'testapp',
  );
  const json = await processStore({
    activeNotifications: [],
    device: selectedDevice,
    pluginStates: {
      [unselectedDeviceClientIdentifier + '#TestDevicePlugin']: {
        msg: 'Test plugin unselected device',
      },
      [selectedDeviceClientIdentifier + '#TestDevicePlugin']: {
        msg: 'Test plugin selected device',
      },
    },
    pluginStates2: {},
    clients: [
      selectedDeviceClient,
      generateClientFromDevice(unselectedDevice, 'testapp'),
    ],
    devicePlugins: new Map(),
    clientPlugins: new Map([['TestDevicePlugin', TestPlugin]]),
    salt: 'salt',
    selectedPlugins: [],
  });

  if (!json) {
    fail('json is undefined');
  }
  const {clients} = json;
  const {pluginStates} = json.store;
  const expectedPluginState = {
    [generateClientIdentifierWithSalt(selectedDeviceClientIdentifier, 'salt') +
    '#TestDevicePlugin']: JSON.stringify({
      msg: 'Test plugin selected device',
    }),
  };
  expect(clients).toEqual([
    generateClientFromClientWithSalt(selectedDeviceClient, 'salt'),
  ]);
  expect(pluginStates).toEqual(expectedPluginState);
});

test('test processStore function to have multiple clients for the selected device', async () => {
  const selectedDevice = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });

  const clientIdentifierApp1 = generateClientIdentifier(
    selectedDevice,
    'testapp1',
  );
  const clientIdentifierApp2 = generateClientIdentifier(
    selectedDevice,
    'testapp2',
  );

  const client1 = generateClientFromDevice(selectedDevice, 'testapp1');
  const client2 = generateClientFromDevice(selectedDevice, 'testapp2');

  const json = await processStore({
    activeNotifications: [],
    device: selectedDevice,
    pluginStates: {
      [clientIdentifierApp1 + '#TestPlugin']: {
        msg: 'Test plugin App1',
      },
      [clientIdentifierApp2 + '#TestPlugin']: {
        msg: 'Test plugin App2',
      },
    },
    pluginStates2: {},
    clients: [
      generateClientFromDevice(selectedDevice, 'testapp1'),
      generateClientFromDevice(selectedDevice, 'testapp2'),
    ],
    devicePlugins: new Map(),
    clientPlugins: new Map([['TestPlugin', TestPlugin]]),
    salt: 'salt',
    selectedPlugins: [],
  });

  if (!json) {
    fail('json is undefined');
  }
  const {clients} = json;
  const {pluginStates} = json.store;
  const expectedPluginState = {
    [generateClientIdentifierWithSalt(clientIdentifierApp1, 'salt') +
    '#TestPlugin']: JSON.stringify({
      msg: 'Test plugin App1',
    }),
    [generateClientIdentifierWithSalt(clientIdentifierApp2, 'salt') +
    '#TestPlugin']: JSON.stringify({
      msg: 'Test plugin App2',
    }),
  };
  expect(clients).toEqual([
    generateClientFromClientWithSalt(client1, 'salt'),
    generateClientFromClientWithSalt(client2, 'salt'),
  ]);
  expect(pluginStates).toEqual(expectedPluginState);
});

test('test processStore function for device plugin state and no clients', async () => {
  // Test case to verify that device plugin data is exported even if there are no clients
  const selectedDevice = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const json = await processStore({
    activeNotifications: [],
    device: selectedDevice,
    pluginStates: {
      'serial#TestDevicePlugin': {
        msg: 'Test Device plugin',
      },
    },
    pluginStates2: {},
    clients: [],
    devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
    clientPlugins: new Map(),
    salt: 'salt',
    selectedPlugins: [],
  });

  if (!json) {
    fail('json is undefined');
  }
  const {pluginStates} = json.store;
  const {clients} = json;
  const expectedPluginState = {
    'salt-serial#TestDevicePlugin': JSON.stringify({msg: 'Test Device plugin'}),
  };
  expect(pluginStates).toEqual(expectedPluginState);
  expect(clients).toEqual([]);
});

test('test processStore function for unselected device plugin state and no clients', async () => {
  // Test case to verify that device plugin data is exported even if there are no clients
  const selectedDevice = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const json = await processStore({
    activeNotifications: [],
    device: selectedDevice,
    pluginStates: {
      'unselectedDeviceIdentifier#TestDevicePlugin': {
        msg: 'Test Device plugin',
      },
    },
    pluginStates2: {},
    clients: [],
    devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
    clientPlugins: new Map(),
    salt: 'salt',
    selectedPlugins: [],
  });
  if (!json) {
    fail('json is undefined');
  }
  const {pluginStates} = json.store;
  const {clients} = json;
  expect(pluginStates).toEqual({});
  expect(clients).toEqual([]);
});

test('test processStore function for notifications for selected device', async () => {
  // Test case to verify that device plugin data is exported even if there are no clients
  const selectedDevice = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const client = generateClientFromDevice(selectedDevice, 'testapp1');
  const notification = generateNotifications(
    'notificationID',
    'title',
    'Notification Message',
    'warning',
  );
  const activeNotification = {
    pluginId: 'TestNotification',
    notification,
    client: client.id,
  };
  const json = await processStore({
    activeNotifications: [activeNotification],
    device: selectedDevice,
    pluginStates: {},
    pluginStates2: {},
    clients: [client],
    devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
    clientPlugins: new Map(),
    salt: 'salt',
    selectedPlugins: [],
  });

  if (!json) {
    fail('json is undefined');
  }
  const {pluginStates} = json.store;
  const {clients} = json;
  expect(pluginStates).toEqual({});
  expect(clients).toEqual([generateClientFromClientWithSalt(client, 'salt')]);
  const {activeNotifications} = json.store;
  const expectedActiveNotification = {
    pluginId: 'TestNotification',
    notification,
    client: generateClientIdentifierWithSalt(client.id, 'salt'),
  };
  expect(activeNotifications).toEqual([expectedActiveNotification]);
});

test('test processStore function for notifications for unselected device', async () => {
  // Test case to verify that device plugin data is exported even if there are no clients
  const selectedDevice = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const unselectedDevice = new ArchivedDevice({
    serial: 'identifier',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });

  const client = generateClientFromDevice(selectedDevice, 'testapp1');
  const unselectedclient = generateClientFromDevice(
    unselectedDevice,
    'testapp1',
  );
  const notification = generateNotifications(
    'notificationID',
    'title',
    'Notification Message',
    'warning',
  );
  const activeNotification = {
    pluginId: 'TestNotification',
    notification,
    client: unselectedclient.id,
  };
  const json = await processStore({
    activeNotifications: [activeNotification],
    device: selectedDevice,
    pluginStates: {},
    pluginStates2: {},
    clients: [client, unselectedclient],
    devicePlugins: new Map(),
    clientPlugins: new Map(),
    salt: 'salt',
    selectedPlugins: [],
  });
  if (!json) {
    fail('json is undefined');
  }
  const {pluginStates} = json.store;
  const {clients} = json;
  expect(pluginStates).toEqual({});
  expect(clients).toEqual([generateClientFromClientWithSalt(client, 'salt')]);
  const {activeNotifications} = json.store;
  expect(activeNotifications).toEqual([]);
});

test('test processStore function for selected plugins', async () => {
  const selectedDevice = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });

  const client = generateClientFromDevice(selectedDevice, 'app');
  const pluginstates = {
    [generateClientIdentifier(selectedDevice, 'app') + '#TestDevicePlugin1']: {
      msg: 'Test plugin1',
    },
    [generateClientIdentifier(selectedDevice, 'app') + '#TestDevicePlugin2']: {
      msg: 'Test plugin2',
    },
  };
  const json = await processStore({
    activeNotifications: [],
    device: selectedDevice,
    pluginStates: pluginstates,
    pluginStates2: {},
    clients: [client],
    devicePlugins: new Map([
      ['TestDevicePlugin1', TestDevicePlugin],
      ['TestDevicePlugin2', TestDevicePlugin],
    ]),
    clientPlugins: new Map(),
    salt: 'salt',
    selectedPlugins: ['TestDevicePlugin2'],
  });
  if (!json) {
    fail('json is undefined');
  }
  const {pluginStates} = json.store;
  const {clients} = json;
  expect(pluginStates).toEqual({
    [generateClientIdentifierWithSalt(
      generateClientIdentifier(selectedDevice, 'app'),
      'salt',
    ) + '#TestDevicePlugin2']: JSON.stringify({
      msg: 'Test plugin2',
    }),
  });
  expect(clients).toEqual([generateClientFromClientWithSalt(client, 'salt')]);
  const {activeNotifications} = json.store;
  expect(activeNotifications).toEqual([]);
});

test('test processStore function for no selected plugins', async () => {
  const selectedDevice = new ArchivedDevice({
    serial: 'serial',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const client = generateClientFromDevice(selectedDevice, 'app');
  const pluginstates = {
    [generateClientIdentifier(selectedDevice, 'app') + '#TestDevicePlugin1']: {
      msg: 'Test plugin1',
    },
    [generateClientIdentifier(selectedDevice, 'app') + '#TestDevicePlugin2']: {
      msg: 'Test plugin2',
    },
  };
  const json = await processStore({
    activeNotifications: [],
    device: selectedDevice,
    pluginStates: pluginstates,
    pluginStates2: {},
    clients: [client],
    devicePlugins: new Map([
      ['TestDevicePlugin1', TestDevicePlugin],
      ['TestDevicePlugin2', TestDevicePlugin],
    ]),
    clientPlugins: new Map(),
    salt: 'salt',
    selectedPlugins: [],
  });

  if (!json) {
    fail('json is undefined');
  }
  const {pluginStates} = json.store;
  const {clients} = json;
  expect(pluginStates).toEqual({
    [generateClientIdentifierWithSalt(
      generateClientIdentifier(selectedDevice, 'app'),
      'salt',
    ) + '#TestDevicePlugin2']: JSON.stringify({
      msg: 'Test plugin2',
    }),
    [generateClientIdentifierWithSalt(
      generateClientIdentifier(selectedDevice, 'app'),
      'salt',
    ) + '#TestDevicePlugin1']: JSON.stringify({
      msg: 'Test plugin1',
    }),
  });
  expect(clients).toEqual([generateClientFromClientWithSalt(client, 'salt')]);
  const {activeNotifications} = json.store;
  expect(activeNotifications).toEqual([]);
});

test('test determinePluginsToProcess for mutilple clients having plugins present', async () => {
  const device1 = new BaseDevice('serial1', 'emulator', 'TestiPhone', 'iOS');
  const client1 = new Client(
    generateClientIdentifier(device1, 'app'),
    {
      app: 'app',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial1',
    },
    null,
    logger,
    mockStore,
    ['TestPlugin', 'TestDevicePlugin'],
    device1,
  );
  const client2 = new Client(
    generateClientIdentifier(device1, 'app2'),
    {
      app: 'app2',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial1',
    },
    null,
    logger,
    mockStore,
    ['TestDevicePlugin'],
    device1,
  );
  const client3 = new Client(
    generateClientIdentifier(device1, 'app3'),
    {
      app: 'app3',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial1',
    },
    null,
    logger,
    mockStore,
    ['TestPlugin', 'TestDevicePlugin'],
    device1,
  );
  const plugins: PluginsState = {
    clientPlugins: new Map([
      ['TestPlugin', TestPlugin],
      ['RandomPlugin', TestPlugin],
    ]),
    devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
    loadedPlugins: new Map([
      ['TestPlugin', TestPlugin.details],
      ['RandomPlugin', TestPlugin.details],
      ['TestDevicePlugin', TestDevicePlugin.details],
    ]),
    bundledPlugins: new Map(),
    gatekeepedPlugins: [],
    disabledPlugins: [],
    failedPlugins: [],
    selectedPlugins: ['TestPlugin'],
    marketplacePlugins: [],
    installedPlugins: new Map(),
    uninstalledPlugins: new Set(),
    initialised: true,
  };
  const op = determinePluginsToProcess(
    [client1, client2, client3],
    device1,
    plugins,
  );
  expect(op).toBeDefined();
  expect(op).toEqual([
    {
      pluginKey: `${client1.id}#TestPlugin`,
      pluginId: 'TestPlugin',
      pluginName: 'TestPlugin',
      pluginClass: TestPlugin,
      client: client1,
    },
    {
      pluginKey: `${client3.id}#TestPlugin`,
      pluginId: 'TestPlugin',
      pluginName: 'TestPlugin',
      pluginClass: TestPlugin,
      client: client3,
    },
  ]);
});

test('test determinePluginsToProcess for no selected plugin present in any clients', async () => {
  const device1 = new BaseDevice('serial1', 'emulator', 'TestiPhone', 'iOS');
  const client1 = new Client(
    generateClientIdentifier(device1, 'app'),
    {
      app: 'app',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial1',
    },
    null,
    logger,
    mockStore,
    ['TestPlugin', 'TestDevicePlugin'],
    device1,
  );
  const client2 = new Client(
    generateClientIdentifier(device1, 'app2'),
    {
      app: 'app2',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial1',
    },
    null,
    logger,
    mockStore,
    ['TestDevicePlugin'],
    device1,
  );
  const plugins: PluginsState = {
    clientPlugins: new Map([
      ['TestPlugin', TestPlugin],
      ['RandomPlugin', TestPlugin],
    ]),
    devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
    loadedPlugins: new Map([
      ['TestPlugin', TestPlugin.details],
      ['RandomPlugin', TestPlugin.details],
      ['TestDevicePlugin', TestDevicePlugin.details],
    ]),
    bundledPlugins: new Map(),
    gatekeepedPlugins: [],
    disabledPlugins: [],
    failedPlugins: [],
    selectedPlugins: ['RandomPlugin'],
    marketplacePlugins: [],
    installedPlugins: new Map(),
    uninstalledPlugins: new Set(),
    initialised: true,
  };
  const op = determinePluginsToProcess([client1, client2], device1, plugins);
  expect(op).toBeDefined();
  expect(op).toEqual([]);
});

test('test determinePluginsToProcess for multiple clients on same device', async () => {
  const device1 = new BaseDevice('serial1', 'emulator', 'TestiPhone', 'iOS');
  const client1 = new Client(
    generateClientIdentifier(device1, 'app'),
    {
      app: 'app',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial1',
    },
    null,
    logger,
    mockStore,
    ['TestPlugin', 'TestDevicePlugin'],
    device1,
  );
  const client2 = new Client(
    generateClientIdentifier(device1, 'app2'),
    {
      app: 'app2',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial1',
    },
    null,
    logger,
    mockStore,
    ['TestDevicePlugin'],
    device1,
  );
  const plugins: PluginsState = {
    clientPlugins: new Map([['TestPlugin', TestPlugin]]),
    devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
    loadedPlugins: new Map([
      ['TestPlugin', TestPlugin.details],
      ['TestDevicePlugin', TestDevicePlugin.details],
    ]),
    bundledPlugins: new Map(),
    gatekeepedPlugins: [],
    disabledPlugins: [],
    failedPlugins: [],
    selectedPlugins: ['TestPlugin'],
    marketplacePlugins: [],
    installedPlugins: new Map(),
    uninstalledPlugins: new Set(),
    initialised: true,
  };
  const op = determinePluginsToProcess([client1, client2], device1, plugins);
  expect(op).toBeDefined();
  expect(op.length).toEqual(1);
  expect(op).toEqual([
    {
      pluginKey: `${client1.id}#TestPlugin`,
      pluginId: 'TestPlugin',
      pluginName: 'TestPlugin',
      pluginClass: TestPlugin,
      client: client1,
    },
  ]);
});

test('test determinePluginsToProcess for multiple clients on different device', async () => {
  const device1 = new BaseDevice('serial1', 'emulator', 'TestiPhone', 'iOS');
  const device2 = new BaseDevice('serial2', 'emulator', 'TestiPhone', 'iOS');
  const client1Device1 = new Client(
    generateClientIdentifier(device1, 'app'),
    {
      app: 'app',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial1',
    },
    null,
    logger,
    mockStore,
    ['TestPlugin', 'TestDevicePlugin'],
    device1,
  );
  const client2Device1 = new Client(
    generateClientIdentifier(device1, 'app2'),
    {
      app: 'app1',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial1',
    },
    null,
    logger,
    mockStore,
    ['TestDevicePlugin'],
    device1,
  );
  const client1Device2 = new Client(
    generateClientIdentifier(device2, 'app'),
    {
      app: 'app',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial2',
    },
    null,
    logger,
    mockStore,
    ['TestPlugin', 'TestDevicePlugin'],
    device1,
  );
  const client2Device2 = new Client(
    generateClientIdentifier(device2, 'app2'),
    {
      app: 'app1',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial2',
    },
    null,
    logger,
    mockStore,
    ['TestDevicePlugin'],
    device1,
  );
  const plugins: PluginsState = {
    clientPlugins: new Map([['TestPlugin', TestPlugin]]),
    devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
    loadedPlugins: new Map([
      ['TestPlugin', TestPlugin.details],
      ['TestDevicePlugin', TestDevicePlugin.details],
    ]),
    bundledPlugins: new Map(),
    gatekeepedPlugins: [],
    disabledPlugins: [],
    failedPlugins: [],
    selectedPlugins: ['TestPlugin'],
    marketplacePlugins: [],
    installedPlugins: new Map(),
    uninstalledPlugins: new Set(),
    initialised: true,
  };
  const op = determinePluginsToProcess(
    [client1Device1, client2Device1, client1Device2, client2Device2],
    device2,
    plugins,
  );
  expect(op).toBeDefined();
  expect(op.length).toEqual(1);
  expect(op).toEqual([
    {
      pluginKey: `${client1Device2.id}#TestPlugin`,
      pluginId: 'TestPlugin',
      pluginName: 'TestPlugin',
      pluginClass: TestPlugin,
      client: client1Device2,
    },
  ]);
});

test('test determinePluginsToProcess to ignore archived clients', async () => {
  const selectedDevice = new BaseDevice(
    'serial',
    'emulator',
    'TestiPhone',
    'iOS',
  );
  const archivedDevice = new ArchivedDevice({
    serial: 'serial-archived',
    deviceType: 'emulator',
    title: 'TestiPhone',
    os: 'iOS',
    screenshotHandle: null,
  });
  const logger = {
    track: () => {},
    info: () => {},
    warn: () => {},
    error: () => {},
    debug: () => {},
    trackTimeSince: () => {},
  };
  const mockStore = configureStore<State, {}>([])();
  const client = new Client(
    generateClientIdentifier(selectedDevice, 'app'),
    {
      app: 'app',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial',
    },
    null,
    logger,
    mockStore,
    ['TestPlugin', 'TestDevicePlugin'],
    archivedDevice,
  );
  const archivedClient = new Client(
    generateClientIdentifier(archivedDevice, 'app'),
    {
      app: 'app',
      os: 'iOS',
      device: 'TestiPhone',
      device_id: 'serial-archived',
    },
    null,
    logger,
    mockStore,
    ['TestPlugin', 'TestDevicePlugin'],
    archivedDevice,
  );
  const plugins: PluginsState = {
    clientPlugins: new Map([['TestPlugin', TestPlugin]]),
    devicePlugins: new Map([['TestDevicePlugin', TestDevicePlugin]]),
    loadedPlugins: new Map([
      ['TestPlugin', TestPlugin.details],
      ['TestDevicePlugin', TestDevicePlugin.details],
    ]),
    bundledPlugins: new Map(),
    gatekeepedPlugins: [],
    disabledPlugins: [],
    failedPlugins: [],
    selectedPlugins: ['TestPlugin'],
    marketplacePlugins: [],
    installedPlugins: new Map(),
    uninstalledPlugins: new Set(),
    initialised: true,
  };
  const op = determinePluginsToProcess(
    [client, archivedClient],
    selectedDevice,
    plugins,
  );
  expect(op).toBeDefined();
  expect(op.length).toEqual(1);
  expect(op).toEqual([
    {
      pluginKey: `${client.id}#TestPlugin`,
      pluginId: 'TestPlugin',
      pluginName: 'TestPlugin',
      pluginClass: TestPlugin,
      client: client,
    },
  ]);
});

const sandyTestPlugin = new _SandyPluginDefinition(
  TestUtils.createMockPluginDetails(),
  {
    plugin(
      client: PluginClient<{
        inc: {};
      }>,
    ) {
      const counter = createState(0, {persist: 'counter'});
      const _somethingElse = createState(0);
      const anotherState = createState({testCount: 0}, {persist: 'otherState'});

      client.onMessage('inc', () => {
        counter.set(counter.get() + 1);
        anotherState.update((draft) => {
          draft.testCount -= 1;
        });
      });
      return {
        counter,
        enableCustomExport() {
          client.onExport(async (idler, onStatus) => {
            if (idler.shouldIdle()) {
              await idler.idle();
            }
            await sleep(100);
            onStatus('hi');
            return {
              customExport: true,
            };
          });
        },
      };
    },
    Component() {
      return null;
    },
  },
);

test('Sandy plugins are exported properly', async () => {
  const {client, sendMessage, store, device} =
    await createMockFlipperWithPlugin(sandyTestPlugin);

  // We do select another plugin, to verify that pending message queues are indeed processed before exporting
  store.dispatch(
    selectPlugin({
      selectedPlugin: 'DeviceLogs',
      selectedApp: null,
      selectedDevice: device,
      deepLinkPayload: null,
    }),
  );

  // Deliberately not using 'act' here, to verify that exportStore itself makes sure buffers are flushed first
  sendMessage('inc', {});
  sendMessage('inc', {});
  sendMessage('inc', {});

  // not flushed
  expect(
    client.sandyPluginStates.get(sandyTestPlugin.id)!.instanceApi.counter.get(),
  ).toBe(0);

  const storeExport = await exportStore(store);
  expect(
    client.sandyPluginStates.get(sandyTestPlugin.id)!.instanceApi.counter.get(),
  ).toBe(3);

  const serial = storeExport.exportStoreData.device!.serial;
  expect(serial).not.toBeFalsy();
  expect(storeExport.exportStoreData.pluginStates2).toEqual({
    [`TestApp#Android#MockAndroidDevice#${serial}`]: {
      TestPlugin: {counter: 3, otherState: {testCount: -3}},
    },
  });
});

test('Non sandy plugins are exported properly if they are still queued', async () => {
  type State = {
    counter: number;
  };

  const {sendMessage, store, device} = await createMockFlipperWithPlugin(
    class TestPlugin extends FlipperPlugin<any, any, State> {
      static id = 'TestPlugin';

      static defaultPersistedState: State = {
        counter: 0,
      };

      static persistedStateReducer(p: State, method: string): State {
        if (method === 'inc') {
          return {
            counter: p.counter + 1,
          };
        }
        return p;
      }
    } as any,
  );

  // We do select another plugin, to verify that pending message queues are indeed processed before exporting
  store.dispatch(
    selectPlugin({
      selectedPlugin: 'DeviceLogs',
      selectedApp: null,
      selectedDevice: device,
      deepLinkPayload: null,
    }),
  );

  // Deliberately not using 'act' here, to verify that exportStore itself makes sure buffers are flushed first
  sendMessage('inc', {});
  sendMessage('inc', {});
  sendMessage('inc', {});

  // not flushed
  expect(store.getState().pluginStates).toMatchInlineSnapshot(`Object {}`);

  const storeExport = await exportStore(store);
  // flushed
  expect(store.getState().pluginStates).toMatchInlineSnapshot(`
    Object {
      "TestApp#Android#MockAndroidDevice#serial#TestPlugin": Object {
        "counter": 3,
      },
    }
  `);

  const serial = storeExport.exportStoreData.device!.serial;
  expect(serial).not.toBeFalsy();
  expect(storeExport.exportStoreData.store.pluginStates).toMatchInlineSnapshot(`
    Object {
      "TestApp#Android#MockAndroidDevice#${serial}#TestPlugin": "{\\"counter\\":3}",
    }
  `);
});

test('Sandy plugins with custom export are exported properly', async () => {
  const {client, sendMessage, store} = await createMockFlipperWithPlugin(
    sandyTestPlugin,
  );

  // We do select another plugin, to verify that pending message queues are indeed processed before exporting
  store.dispatch(
    selectPlugin({
      selectedPlugin: 'DeviceLogs',
      selectedApp: client.id,
      deepLinkPayload: null,
    }),
  );

  client.sandyPluginStates
    .get(sandyTestPlugin.id)
    ?.instanceApi.enableCustomExport();

  // Deliberately not using 'act' here, to verify that exportStore itself makes sure buffers are flushed first
  sendMessage('inc', {});
  sendMessage('inc', {});
  sendMessage('inc', {});

  const storeExport = await exportStore(store);
  const serial = storeExport.exportStoreData.device!.serial;
  expect(serial).not.toBeFalsy();
  expect(storeExport.exportStoreData.pluginStates2).toEqual({
    [`TestApp#Android#MockAndroidDevice#${serial}`]: {
      TestPlugin: {customExport: true},
    },
  });
});

test('Sandy plugins are imported properly', async () => {
  const data = {
    clients: [
      {
        id: 'TestApp#Android#MockAndroidDevice#2e52cea6-94b0-4ea1-b9a8-c9135ede14ca-serial',
        query: {
          app: 'TestApp',
          device: 'MockAndroidDevice',
          device_id: '2e52cea6-94b0-4ea1-b9a8-c9135ede14ca-serial',
          os: 'Android',
          sdk_version: 4,
        },
      },
    ],
    device: {
      deviceType: 'physical',
      logs: [],
      os: 'Android',
      serial: '2e52cea6-94b0-4ea1-b9a8-c9135ede14ca-serial',
      title: 'MockAndroidDevice',
    },
    deviceScreenshot: null,
    fileVersion: '0.9.99',
    flipperReleaseRevision: undefined,
    pluginStates2: {
      'TestApp#Android#MockAndroidDevice#2e52cea6-94b0-4ea1-b9a8-c9135ede14ca-serial':
        {
          TestPlugin: {
            otherState: {
              testCount: -3,
            },
            counter: 3,
          },
        },
    },
    store: {
      activeNotifications: [],
      pluginStates: {},
    },
  };

  const {client, store} = await createMockFlipperWithPlugin(sandyTestPlugin);

  await importDataToStore('unittest.json', JSON.stringify(data), store);

  const client2 = store.getState().connections.clients[1];
  expect(client2).not.toBeFalsy();
  expect(client2).not.toBe(client);
  expect(client2.plugins).toEqual([TestPlugin.id]);

  expect(client.sandyPluginStates.get(TestPlugin.id)!.exportStateSync())
    .toMatchInlineSnapshot(`
    Object {
      "counter": 0,
      "otherState": Object {
        "testCount": 0,
      },
    }
  `);
  expect(client2.sandyPluginStates.get(TestPlugin.id)!.exportStateSync())
    .toMatchInlineSnapshot(`
    Object {
      "counter": 3,
      "otherState": Object {
        "testCount": -3,
      },
    }
  `);
});

const sandyDeviceTestPlugin = new _SandyPluginDefinition(
  TestUtils.createMockPluginDetails({pluginType: 'device'}),
  {
    supportsDevice: () => true,
    devicePlugin(client: DevicePluginClient) {
      const counter = createState(0, {persist: 'counter'});
      const _somethingElse = createState(0);
      const anotherState = createState({testCount: 0}, {persist: 'otherState'});

      client.device.onLogEntry(() => {
        counter.set(counter.get() + 1);
        anotherState.update((draft) => {
          draft.testCount -= 1;
        });
      });
      return {
        counter,
        enableCustomExport() {
          client.onExport(async (idler, onStatus) => {
            if (idler.shouldIdle()) {
              await idler.idle();
            }
            onStatus('hi');
            await sleep(100);
            return {
              customExport: true,
            };
          });
        },
      };
    },
    Component() {
      return null;
    },
  },
);

test('Sandy device plugins are exported / imported properly', async () => {
  const data = {
    clients: [],
    device: {
      deviceType: 'archivedPhysical',
      logs: [],
      os: 'Android',
      serial: '2e52cea6-94b0-4ea1-b9a8-c9135ede14ca-serial',
      title: 'MockAndroidDevice',
      pluginStates: {
        [sandyDeviceTestPlugin.id]: {
          otherState: {
            testCount: -3,
          },
          counter: 3,
        },
      },
    },
    deviceScreenshot: null,
    fileVersion: '0.9.99',
    flipperReleaseRevision: undefined,
    pluginStates2: {},
    store: {
      activeNotifications: [],
      pluginStates: {},
    },
  };

  const {device, store} = await createMockFlipperWithPlugin(
    sandyDeviceTestPlugin,
  );

  await importDataToStore('unittest.json', JSON.stringify(data), store);

  const device2 = store.getState().connections.devices[1];
  expect(device2).not.toBeFalsy();
  expect(device2).not.toBe(device);
  expect(device2.devicePlugins).toEqual([sandyDeviceTestPlugin.id]);

  const {counter} = device2.sandyPluginStates.get(
    sandyDeviceTestPlugin.id,
  )?.instanceApi;
  counter.set(counter.get() + 1);

  expect(
    (
      await device.exportState(testIdler, testOnStatusMessage, [
        sandyDeviceTestPlugin.id,
      ])
    )[sandyDeviceTestPlugin.id],
  ).toMatchInlineSnapshot(`
    Object {
      "counter": 0,
      "otherState": Object {
        "testCount": 0,
      },
    }
  `);
  expect(
    await device2.exportState(testIdler, testOnStatusMessage, [
      sandyDeviceTestPlugin.id,
    ]),
  ).toMatchInlineSnapshot(`
    Object {
      "TestPlugin": Object {
        "counter": 4,
        "otherState": Object {
          "testCount": -3,
        },
      },
    }
  `);
});

test('Sandy device plugins with custom export are export properly', async () => {
  const {device, store} = await createMockFlipperWithPlugin(
    sandyDeviceTestPlugin,
  );

  device.sandyPluginStates
    .get(sandyDeviceTestPlugin.id)
    ?.instanceApi.enableCustomExport();

  store.dispatch(selectedPlugins([sandyDeviceTestPlugin.id]));
  const storeExport = await exportStore(store);
  expect(storeExport.exportStoreData.device!.pluginStates).toEqual({
    [sandyDeviceTestPlugin.id]: {customExport: true},
  });
});

test('Sandy plugin with custom import', async () => {
  const plugin = new _SandyPluginDefinition(
    TestUtils.createMockPluginDetails(),
    {
      plugin(client: PluginClient) {
        const counter = createState(0);
        client.onImport((data) => {
          counter.set(data.count);
        });

        return {
          counter,
        };
      },
      Component() {
        return null;
      },
    },
  );

  const {store} = await createMockFlipperWithPlugin(plugin);

  const data = {
    clients: [
      {
        id: 'TestApp#Android#MockAndroidDevice#2e52cea6-94b0-4ea1-b9a8-c9135ede14ca-serial',
        query: {
          app: 'TestApp',
          device: 'MockAndroidDevice',
          device_id: '2e52cea6-94b0-4ea1-b9a8-c9135ede14ca-serial',
          os: 'Android',
          sdk_version: 4,
        },
      },
    ],
    device: {
      deviceType: 'physical',
      logs: [],
      os: 'Android',
      serial: '2e52cea6-94b0-4ea1-b9a8-c9135ede14ca-serial',
      title: 'MockAndroidDevice',
    },
    deviceScreenshot: null,
    fileVersion: '0.9.99',
    flipperReleaseRevision: undefined,
    pluginStates2: {
      'TestApp#Android#MockAndroidDevice#2e52cea6-94b0-4ea1-b9a8-c9135ede14ca-serial':
        {
          [plugin.id]: {
            count: 4,
          },
        },
    },
    store: {
      activeNotifications: [],
      pluginStates: {},
    },
  };

  await importDataToStore('unittest.json', JSON.stringify(data), store);

  expect(
    store
      .getState()
      .connections.clients[0].sandyPluginStates.get(plugin.id)
      ?.instanceApi.counter.get(),
  ).toBe(0);
  expect(
    store
      .getState()
      .connections.clients[1].sandyPluginStates.get(plugin.id)
      ?.instanceApi.counter.get(),
  ).toBe(4);
});

test('Sandy device plugin with custom import', async () => {
  const plugin = new _SandyPluginDefinition(
    TestUtils.createMockPluginDetails({pluginType: 'device'}),
    {
      supportsDevice: () => true,
      devicePlugin(client: DevicePluginClient) {
        const counter = createState(0);
        client.onImport((data) => {
          counter.set(data.count);
        });

        return {
          counter,
        };
      },
      Component() {
        return null;
      },
    },
  );

  const data = {
    clients: [],
    device: {
      deviceType: 'archivedPhysical',
      logs: [],
      os: 'Android',
      serial: '2e52cea6-94b0-4ea1-b9a8-c9135ede14ca-serial',
      title: 'MockAndroidDevice',
      pluginStates: {
        [plugin.id]: {
          count: 2,
        },
      },
    },
    deviceScreenshot: null,
    fileVersion: '0.9.99',
    flipperReleaseRevision: undefined,
    pluginStates2: {},
    store: {
      activeNotifications: [],
      pluginStates: {},
    },
  };

  const {store} = await createMockFlipperWithPlugin(plugin);

  await importDataToStore('unittest.json', JSON.stringify(data), store);

  expect(
    store
      .getState()
      .connections.devices[0].sandyPluginStates.get(plugin.id)
      ?.instanceApi.counter.get(),
  ).toBe(0);
  expect(
    store
      .getState()
      .connections.devices[1].sandyPluginStates.get(plugin.id)
      ?.instanceApi.counter.get(),
  ).toBe(2);
});

test('Sandy plugins with complex data are imported  / exported correctly', async () => {
  const plugin = new _SandyPluginDefinition(
    TestUtils.createMockPluginDetails(),
    {
      plugin() {
        const m = createState(new Map([['a', 1]]), {persist: 'map'});
        const s = createState(new Set([{x: 2}]), {persist: 'set'});
        const d = createState(new Date(1611913002865), {persist: 'date'});
        return {
          m,
          s,
          d,
        };
      },
      Component() {
        return null;
      },
    },
  );

  const {store, client} = await createMockFlipperWithPlugin(plugin);

  client.disconnect(); // lets make sure we can still export disconnected clients
  const data = await exportStore(store);
  expect(Object.values(data.exportStoreData.pluginStates2)).toMatchObject([
    {
      TestPlugin: {
        date: {
          __flipper_object_type__: 'Date',
          // no data asserted, since that is TZ sensitve
        },
        map: {
          __flipper_object_type__: 'Map',
          data: [['a', 1]],
        },
        set: {
          __flipper_object_type__: 'Set',
          data: [
            {
              x: 2,
            },
          ],
        },
      },
    },
  ]);

  await importDataToStore('unittest.json', data.serializedString, store);
  const api = store
    .getState()
    .connections.clients[1].sandyPluginStates.get(plugin.id)?.instanceApi;
  expect(api.m.get()).toMatchInlineSnapshot(`
    Map {
      "a" => 1,
    }
  `);
  expect(api.s.get()).toMatchInlineSnapshot(`
    Set {
      Object {
        "x": 2,
      },
    }
  `);
  expect(api.d.get()).toEqual(new Date(1611913002865));
});

test('Sandy device plugins with complex data are imported  / exported correctly', async () => {
  const deviceplugin = new _SandyPluginDefinition(
    TestUtils.createMockPluginDetails({
      id: 'deviceplugin',
      pluginType: 'device',
    }),
    {
      supportsDevice() {
        return true;
      },
      devicePlugin() {
        const m = createState(new Map([['a', 1]]), {persist: 'map'});
        const s = createState(new Set([{x: 2}]), {persist: 'set'});
        const d = createState(new Date(1611913002865), {persist: 'date'});
        return {
          m,
          s,
          d,
        };
      },
      Component() {
        return null;
      },
    },
  );

  const {store} = await createMockFlipperWithPlugin(deviceplugin);
  store.dispatch(selectedPlugins([deviceplugin.id]));

  const data = await exportStore(store);
  expect(data.exportStoreData.device?.pluginStates).toMatchObject({
    deviceplugin: {
      date: {
        __flipper_object_type__: 'Date',
        // no data asserted, since that is TZ sensitve
      },
      map: {
        __flipper_object_type__: 'Map',
        data: [['a', 1]],
      },
      set: {
        __flipper_object_type__: 'Set',
        data: [
          {
            x: 2,
          },
        ],
      },
    },
  });
  await importDataToStore('unittest.json', data.serializedString, store);
  const api = store
    .getState()
    .connections.devices[1].sandyPluginStates.get(deviceplugin.id)?.instanceApi;
  expect(api.m.get()).toMatchInlineSnapshot(`
    Map {
      "a" => 1,
    }
  `);
  expect(api.s.get()).toMatchInlineSnapshot(`
    Set {
      Object {
        "x": 2,
      },
    }
  `);
  expect(api.d.get()).toEqual(new Date(1611913002865));
});
