import sinon from 'sinon';

import {
  dropQueue,
  callBackgroundMethod,
  submitRequestToBackground,
  _setBackgroundConnection,
} from '.';

// This file tests only MV3 queue scenario
// MV2 tests are already covered by '../actions.test.js'

jest.mock('../../../shared/modules/mv3.utils', () => {
  return {
    isManifestV3: () => true,
  };
});

describe('ActionQueue', () => {
  afterEach(() => {
    sinon.restore();
    dropQueue(true);
  });

  describe('dropQueue', () => {
    it('rejects all pending actions by default', async () => {
      const background = {
        connectionStream: {
          readable: false,
        },
        backgroundFunction: sinon.stub().yields(),
      };

      _setBackgroundConnection(background);
      const result = submitRequestToBackground('backgroundFunction');
      dropQueue();

      await expect(result).rejects.toThrow(
        'Background operation cancelled while waiting for connection.',
      );
      expect(background.backgroundFunction.called).toStrictEqual(false);
    });
  });

  describe('submitRequestToBackground', () => {
    it('calls promisified background method if the stream is connected', async () => {
      const background = {
        connectionStream: {
          readable: true,
        },
        backgroundFunction1: sinon.stub().yields(),
      };

      _setBackgroundConnection(background);
      submitRequestToBackground('backgroundFunction1');
      expect(background.backgroundFunction1.called).toStrictEqual(true);
    });

    it('does not calls promisified background method if the stream is not connected', async () => {
      const background = {
        connectionStream: {
          readable: false,
        },
        backgroundFunction2: sinon.stub().yields(),
      };

      _setBackgroundConnection(background);
      submitRequestToBackground('backgroundFunction2');
      expect(background.backgroundFunction2.called).toStrictEqual(false);
    });

    it('calls promisified background method on stream reconnection', async () => {
      const background = {
        connectionStream: {
          readable: false,
        },
        backgroundFunction3: sinon.stub().yields(),
        trackMetaMetricsEvent: sinon.stub().yields(),
      };
      _setBackgroundConnection(background);
      const requestPromise = submitRequestToBackground('backgroundFunction3');

      background.connectionStream = {
        readable: true,
      };
      _setBackgroundConnection(background);
      await requestPromise;
      expect(background.backgroundFunction3.calledOnce).toStrictEqual(true);
    });

    it('resolves if backgroundFunction resolves', async () => {
      const background = {
        connectionStream: {
          readable: true,
        },
        backgroundFunction4: (cb) => {
          return cb(null, 'test');
        },
      };
      _setBackgroundConnection(background);
      await expect(
        submitRequestToBackground('backgroundFunction4'),
      ).resolves.toStrictEqual('test');
    });

    it('rejects if backgroundFunction throws exception', async () => {
      expect.assertions(1);
      const background = {
        connectionStream: {
          readable: true,
        },
        backgroundFunction: () => {
          throw Error('test');
        },
      };
      _setBackgroundConnection(background);
      await expect(
        submitRequestToBackground('backgroundFunction'),
      ).rejects.toThrow('test');
    });

    it('calls methods in parallel when connection available', async () => {
      const trace = {};
      const background = {
        connectionStream: {
          readable: true,
        },
        first: (cb) => {
          setTimeout(() => {
            trace.firstDone = Date.now();
            cb(null, 'first');
          }, 5);
        },
        second: (cb) => {
          trace.secondStarted = Date.now();
          setTimeout(() => cb(null, 'second'), 10);
        },
        trackMetaMetricsEvent: sinon.stub().yields(),
      };
      _setBackgroundConnection(background);
      const scheduled = Promise.all([
        submitRequestToBackground('first'),
        submitRequestToBackground('second'),
      ]);
      await scheduled;
      expect(trace.firstDone).toBeGreaterThan(trace.secondStarted);
    });

    it('processes the queue sequentially when connection is restored', async () => {
      const trace = {};
      const background = {
        connectionStream: {
          readable: false,
        },
        first: (cb) => {
          setTimeout(() => {
            trace.firstDone = Date.now();
            cb(null, 'first');
          }, 5);
        },
        second: (cb) => {
          trace.secondStarted = Date.now();
          setTimeout(() => cb(null, 'second'), 10);
        },
        trackMetaMetricsEvent: sinon.stub().yields(),
      };
      _setBackgroundConnection(background);
      const scheduled = Promise.all([
        submitRequestToBackground('first'),
        submitRequestToBackground('second'),
      ]);
      background.connectionStream.readable = true;
      _setBackgroundConnection(background);
      await scheduled;
      expect(trace.firstDone).toBeLessThanOrEqual(trace.secondStarted);
    });

    it('ensures actions in queue will not repeat once finished', async () => {
      const trace = { calls: 0 };
      const background = {
        connectionStream: {
          readable: false,
        },
        first: (cb) => {
          trace.calls += 1;
          setTimeout(() => {
            trace.firstDone = Date.now();
            cb(null, 'first');
          }, 5);
        },
        second: (cb) => {
          trace.calls += 1;
          trace.secondStarted = Date.now();
          setTimeout(() => cb(null, 'second'), 10);
        },
        trackMetaMetricsEvent: sinon.stub().yields(),
      };
      _setBackgroundConnection(background);
      const scheduled = Promise.all([
        submitRequestToBackground('first'),
        submitRequestToBackground('second'),
      ]);
      background.connectionStream.readable = true;
      _setBackgroundConnection(background);
      await scheduled;
      _setBackgroundConnection(background); // once all actions finished, this triggers draining the queue again
      expect(trace.firstDone).toBeLessThanOrEqual(trace.secondStarted);
      expect(trace.calls).toStrictEqual(2);
    });

    it('stops processng the queue if connection is lost', async () => {
      const trace = {};
      const background = {
        connectionStream: {
          readable: false,
        },
        first: (cb) => {
          setTimeout(() => {
            trace.firstDone = true;
            background.connectionStream.readable = false;
            cb(Error('lost connection'));
          }, 5);
        },
        second: sinon.stub().yields(),
        trackMetaMetricsEvent: sinon.stub().yields(),
      };
      _setBackgroundConnection(background);
      const scheduled = Promise.race([
        submitRequestToBackground('first').catch(() => ({})),
        submitRequestToBackground('second'),
      ]);
      background.connectionStream.readable = true;
      _setBackgroundConnection(background);
      await scheduled;
      await Promise.resolve('one more tick'); // One asynchronous tick to avoid depending on implementation details
      expect(trace.firstDone).toStrictEqual(true);
      expect(background.second.called).toStrictEqual(false);
    });

    // Failing test for a race condition related to how items are removed from queue
    it('avoids race conditions', async () => {
      const trace = { first: 0, second: 0 };
      const flowControl = {};
      const background = {
        connectionStream: {
          readable: false,
        },
        first: (cb) => {
          trace.first += 1;
          setTimeout(() => {
            flowControl.triggerRaceCondition();
            cb(null, 'first');
          }, 5);
        },
        second: (cb) => {
          trace.second += 1;
          setTimeout(() => cb(null, 'second'), 10);
        },
        third: sinon.stub().yields(),
        trackMetaMetricsEvent: sinon.stub().yields(),
      };
      flowControl.triggerRaceCondition = () => {
        flowControl.waitFor = submitRequestToBackground('third');
      };
      _setBackgroundConnection(background);
      const scheduled = Promise.all([
        submitRequestToBackground('first'),
        submitRequestToBackground('second'),
      ]);
      background.connectionStream.readable = true;
      _setBackgroundConnection(background);
      await scheduled;
      await flowControl.waitFor;
      expect(trace.first).toStrictEqual(1);
      expect(trace.second).toStrictEqual(1);
      expect(background.third.calledOnce).toStrictEqual(true);
    });
  });

  describe('callBackgroundMethod', () => {
    afterEach(() => {
      sinon.restore();
    });

    it('calls background method if the stream is connected', async () => {
      const background = {
        connectionStream: {
          readable: true,
        },
        backgroundFunction: sinon.stub().yields(),
      };

      _setBackgroundConnection(background);
      callBackgroundMethod('backgroundFunction', [], () => ({}));
      expect(background.backgroundFunction.called).toStrictEqual(true);
    });

    it('does not call background method if the stream is not connected', async () => {
      const background = {
        connectionStream: {
          readable: false,
        },
        backgroundFunction: sinon.stub(),
      };

      _setBackgroundConnection(background);
      callBackgroundMethod('backgroundFunction', [], () => ({}));
      expect(background.backgroundFunction.called).toStrictEqual(false);
    });

    it('calls background method on stream reconnection', async () => {
      const background = {
        connectionStream: {
          readable: false,
        },
        backgroundFunction: sinon.stub().yields(),
      };
      _setBackgroundConnection(background);
      callBackgroundMethod('backgroundFunction', [], () => ({}));
      expect(background.backgroundFunction.called).toStrictEqual(false);

      background.connectionStream = {
        readable: true,
      };
      _setBackgroundConnection(background);
      expect(background.backgroundFunction.calledOnce).toStrictEqual(true);
    });

    it('resolves if backgroundFunction called resolves', async () => {
      const background = {
        connectionStream: {
          readable: true,
        },
        backgroundFunction: (cb) => {
          return cb(null, 'successViaCallback');
        },
      };
      _setBackgroundConnection(background);
      const value = await new Promise((resolve) => {
        callBackgroundMethod('backgroundFunction', [], (_err, result) => {
          resolve(result);
        });
      });
      expect(value).toStrictEqual('successViaCallback');
    });
    it('rejects if backgroundFunction called rejects', async () => {
      const errorViaCallback = Error('errorViaCallback');
      const background = {
        connectionStream: {
          readable: true,
        },
        backgroundFunction: (cb) => {
          return cb(errorViaCallback);
        },
      };
      _setBackgroundConnection(background);
      const value = await new Promise((resolve) => {
        callBackgroundMethod('backgroundFunction', [], (err) => {
          resolve(err);
        });
      });
      expect(value).toStrictEqual(errorViaCallback);
    });
  });
});
