import waitForPromises from 'helpers/wait_for_promises';
import Poll from '~/lib/utils/poll';
import { successCodes } from '~/lib/utils/http_status';

describe('Poll', () => {
  let callbacks;
  let service;

  function setup() {
    return new Poll({
      resource: service,
      method: 'fetch',
      successCallback: callbacks.success,
      errorCallback: callbacks.error,
      notificationCallback: callbacks.notification,
    }).makeRequest();
  }

  const mockServiceCall = (response, shouldFail = false) => {
    const value = {
      ...response,
      header: response.header || {},
    };

    if (shouldFail) {
      service.fetch.mockRejectedValue(value);
    } else {
      service.fetch.mockResolvedValue(value);
    }
  };

  const waitForAllCallsToFinish = (waitForCount, successCallback) => {
    if (!waitForCount) {
      return Promise.resolve().then(successCallback());
    }

    jest.runOnlyPendingTimers();

    return waitForPromises().then(() => waitForAllCallsToFinish(waitForCount - 1, successCallback));
  };

  beforeEach(() => {
    service = {
      fetch: jest.fn(),
    };
    callbacks = {
      success: jest.fn(),
      error: jest.fn(),
      notification: jest.fn(),
    };
  });

  it('calls the success callback when no header for interval is provided', done => {
    mockServiceCall({ status: 200 });
    setup();

    waitForAllCallsToFinish(1, () => {
      expect(callbacks.success).toHaveBeenCalled();
      expect(callbacks.error).not.toHaveBeenCalled();

      done();
    });
  });

  it('calls the error callback when the http request returns an error', done => {
    mockServiceCall({ status: 500 }, true);
    setup();

    waitForAllCallsToFinish(1, () => {
      expect(callbacks.success).not.toHaveBeenCalled();
      expect(callbacks.error).toHaveBeenCalled();

      done();
    });
  });

  it('skips the error callback when request is aborted', done => {
    mockServiceCall({ status: 0 }, true);
    setup();

    waitForAllCallsToFinish(1, () => {
      expect(callbacks.success).not.toHaveBeenCalled();
      expect(callbacks.error).not.toHaveBeenCalled();
      expect(callbacks.notification).toHaveBeenCalled();

      done();
    });
  });

  it('should call the success callback when the interval header is -1', done => {
    mockServiceCall({ status: 200, headers: { 'poll-interval': -1 } });
    setup()
      .then(() => {
        expect(callbacks.success).toHaveBeenCalled();
        expect(callbacks.error).not.toHaveBeenCalled();

        done();
      })
      .catch(done.fail);
  });

  describe('for 2xx status code', () => {
    successCodes.forEach(httpCode => {
      it(`starts polling when http status is ${httpCode} and interval header is provided`, done => {
        mockServiceCall({ status: httpCode, headers: { 'poll-interval': 1 } });

        const Polling = new Poll({
          resource: service,
          method: 'fetch',
          data: { page: 1 },
          successCallback: callbacks.success,
          errorCallback: callbacks.error,
        });

        Polling.makeRequest();

        waitForAllCallsToFinish(2, () => {
          Polling.stop();

          expect(service.fetch.mock.calls).toHaveLength(2);
          expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
          expect(callbacks.success).toHaveBeenCalled();
          expect(callbacks.error).not.toHaveBeenCalled();

          done();
        });
      });
    });
  });

  describe('with delayed initial request', () => {
    it('delays the first request', async done => {
      mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });

      const Polling = new Poll({
        resource: service,
        method: 'fetch',
        data: { page: 1 },
        successCallback: callbacks.success,
        errorCallback: callbacks.error,
      });

      Polling.makeDelayedRequest(1);

      expect(Polling.timeoutID).toBeTruthy();

      waitForAllCallsToFinish(2, () => {
        Polling.stop();

        expect(service.fetch.mock.calls).toHaveLength(2);
        expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
        expect(callbacks.success).toHaveBeenCalled();
        expect(callbacks.error).not.toHaveBeenCalled();

        done();
      });
    });
  });

  describe('stop', () => {
    it('stops polling when method is called', done => {
      mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });

      const Polling = new Poll({
        resource: service,
        method: 'fetch',
        data: { page: 1 },
        successCallback: () => {
          Polling.stop();
        },
        errorCallback: callbacks.error,
      });

      jest.spyOn(Polling, 'stop');

      Polling.makeRequest();

      waitForAllCallsToFinish(1, () => {
        expect(service.fetch.mock.calls).toHaveLength(1);
        expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
        expect(Polling.stop).toHaveBeenCalled();

        done();
      });
    });
  });

  describe('enable', () => {
    it('should enable polling upon a response', done => {
      mockServiceCall({ status: 200 });
      const Polling = new Poll({
        resource: service,
        method: 'fetch',
        data: { page: 1 },
        successCallback: () => {},
      });

      Polling.enable({
        data: { page: 4 },
        response: { status: 200, headers: { 'poll-interval': 1 } },
      });

      waitForAllCallsToFinish(1, () => {
        Polling.stop();

        expect(service.fetch.mock.calls).toHaveLength(1);
        expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
        expect(Polling.options.data).toEqual({ page: 4 });
        done();
      });
    });
  });

  describe('restart', () => {
    it('should restart polling when its called', done => {
      mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });

      const Polling = new Poll({
        resource: service,
        method: 'fetch',
        data: { page: 1 },
        successCallback: () => {
          Polling.stop();

          // Let's pretend that we asynchronously restart this.
          // setTimeout is mocked but this will actually get triggered
          // in waitForAllCalssToFinish.
          setTimeout(() => {
            Polling.restart({ data: { page: 4 } });
          }, 1);
        },
        errorCallback: callbacks.error,
      });

      jest.spyOn(Polling, 'stop');
      jest.spyOn(Polling, 'enable');
      jest.spyOn(Polling, 'restart');

      Polling.makeRequest();

      waitForAllCallsToFinish(2, () => {
        Polling.stop();

        expect(service.fetch.mock.calls).toHaveLength(2);
        expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
        expect(Polling.stop).toHaveBeenCalled();
        expect(Polling.enable).toHaveBeenCalled();
        expect(Polling.restart).toHaveBeenCalled();
        expect(Polling.options.data).toEqual({ page: 4 });
        done();
      });
    });
  });
});