// Imports
import * as nock from 'nock';
import {GithubApi} from '../../lib/common/github-api';

// Tests
describe('GithubApi', () => {
  let api: GithubApi;

  beforeEach(() => api = new GithubApi('12345'));


  describe('constructor()', () => {

    it('should throw if \'githubToken\' is missing or empty', () => {
      expect(() => new GithubApi('')).toThrowError('Missing or empty required parameter \'githubToken\'!');
    });

  });


  describe('get()', () => {
    let apiBuildPathSpy: jasmine.Spy;
    let apiRequestSpy: jasmine.Spy;

    beforeEach(() => {
      apiBuildPathSpy = spyOn(api as any, 'buildPath');
      apiRequestSpy = spyOn(api as any, 'request');
    });


    it('should call \'buildPath()\' with the pathname and params', () => {
      api.get('/foo', {bar: 'baz'});

      expect(apiBuildPathSpy).toHaveBeenCalled();
      expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]);
    });


    it('should call \'request()\' with the correct method', () => {
      api.get('/foo');

      expect(apiRequestSpy).toHaveBeenCalled();
      expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('get');
    });


    it('should call \'request()\' with the correct path', () => {
      apiBuildPathSpy.and.returnValue('/foo/bar');
      api.get('foo');

      expect(apiRequestSpy).toHaveBeenCalled();
      expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar');
    });


    it('should not pass data to \'request()\'', () => {
      (api.get as any)('foo', {}, {});

      expect(apiRequestSpy).toHaveBeenCalled();
      expect(apiRequestSpy.calls.argsFor(0)[2]).toBeUndefined();
    });

  });


  describe('post()', () => {
    let apiBuildPathSpy: jasmine.Spy;
    let apiRequestSpy: jasmine.Spy;

    beforeEach(() => {
      apiBuildPathSpy = spyOn(api as any, 'buildPath');
      apiRequestSpy = spyOn(api as any, 'request');
    });


    it('should call \'buildPath()\' with the pathname and params', () => {
      api.post('/foo', {bar: 'baz'});

      expect(apiBuildPathSpy).toHaveBeenCalled();
      expect(apiBuildPathSpy.calls.argsFor(0)).toEqual(['/foo', {bar: 'baz'}]);
    });


    it('should call \'request()\' with the correct method', () => {
      api.post('/foo');

      expect(apiRequestSpy).toHaveBeenCalled();
      expect(apiRequestSpy.calls.argsFor(0)[0]).toBe('post');
    });


    it('should call \'request()\' with the correct path', () => {
      apiBuildPathSpy.and.returnValue('/foo/bar');
      api.post('/foo');

      expect(apiRequestSpy).toHaveBeenCalled();
      expect(apiRequestSpy.calls.argsFor(0)[1]).toBe('/foo/bar');
    });


    it('should pass the data to \'request()\'', () => {
      api.post('/foo', {}, {bar: 'baz'});

      expect(apiRequestSpy).toHaveBeenCalled();
      expect(apiRequestSpy.calls.argsFor(0)[2]).toEqual({bar: 'baz'});
    });

  });


  describe('getPaginated()', () => {
    let deferreds: {resolve: (v: any) => void, reject: (v: any) => void}[];

    beforeEach(() => {
      deferreds = [];
      spyOn(api, 'get').and.callFake(() => new Promise((resolve, reject) => deferreds.push({resolve, reject})));
    });


    it('should return a promise', () => {
      expect((api as any).getPaginated()).toBeInstanceOf(Promise);
    });


    it('should call \'get()\' with the correct pathname and params', () => {
      (api as any).getPaginated('/foo/bar');
      (api as any).getPaginated('/foo/bar', {baz: 'qux'});

      expect(api.get).toHaveBeenCalledWith('/foo/bar', {page: 1, per_page: 100});
      expect(api.get).toHaveBeenCalledWith('/foo/bar', {baz: 'qux', page: 1, per_page: 100});
    });


    it('should reject if the request fails', async () => {
      const responsePromise = (api as any).getPaginated('/foo/bar');
      deferreds[0].reject('Test');

      await expectAsync(responsePromise).toBeRejectedWith('Test');
    });


    it('should resolve with the returned items', async () => {
      const items = [{id: 1}, {id: 2}];
      const responsePromise = (api as any).getPaginated('/foo/bar');
      deferreds[0].resolve(items);

      await expectAsync(responsePromise).toBeResolvedTo(items);
    });


    it('should iteratively call \'get()\' to fetch all items', async () => {
      // Create an array or 250 objects.
      const allItems = new Array(250).fill(null).map((_, i) => ({id: i}));
      const apiGetSpy = api.get as jasmine.Spy;
      const paramsForPage = (page: number) => ({baz: 'qux', page, per_page: 100});

      const responsePromise = (api as any).getPaginated('/foo/bar', {baz: 'qux'});

      deferreds[0].resolve(allItems.slice(0, 100));
      setTimeout(() => {
        deferreds[1].resolve(allItems.slice(100, 200));
        setTimeout(() => {
          deferreds[2].resolve(allItems.slice(200));
        }, 0);
      }, 0);

      await expectAsync(responsePromise).toBeResolvedTo(allItems);

      expect(apiGetSpy).toHaveBeenCalledTimes(3);
      expect(apiGetSpy.calls.argsFor(0)).toEqual(['/foo/bar', paramsForPage(1)]);
      expect(apiGetSpy.calls.argsFor(1)).toEqual(['/foo/bar', paramsForPage(2)]);
      expect(apiGetSpy.calls.argsFor(2)).toEqual(['/foo/bar', paramsForPage(3)]);
    });

  });


  // Protected methods

  describe('buildPath()', () => {

    it('should return the pathname if no params', () => {
      expect((api as any).buildPath('/foo')).toBe('/foo');
      expect((api as any).buildPath('/foo', undefined)).toBe('/foo');
      expect((api as any).buildPath('/foo', null)).toBe('/foo');
    });


    it('should append the params to the pathname', () => {
      expect((api as any).buildPath('/foo', {bar: 'baz'})).toBe('/foo?bar=baz');
    });


    it('should join the params with \'&\'', () => {
      expect((api as any).buildPath('/foo', {bar: 1, baz: 2})).toBe('/foo?bar=1&baz=2');
    });


    it('should ignore undefined/null params', () => {
      expect((api as any).buildPath('/foo', {bar: undefined, baz: null})).toBe('/foo');
    });


    it('should encode param values as URI components', () => {
      expect((api as any).buildPath('/foo', {bar: 'b a&z'})).toBe('/foo?bar=b%20a%26z');
    });

  });

  describe('request()', () => {
    it('should return a promise', () => {
      nock('https://api.github.com').get('/').reply(200);
      expect((api as any).request()).toBeInstanceOf(Promise);
    });


    it('should call \'https.request()\' with the correct options', () => {
      const requestHandler = nock('https://api.github.com')
        .intercept('/path', 'method')
        .reply(200);

      (api as any).request('method', '/path');
      requestHandler.done();
    });


    it('should add the \'Authorization\' header containing the \'githubToken\'', () => {
      const requestHandler = nock('https://api.github.com')
        .intercept('/path', 'method', undefined, {
          reqheaders: {Authorization: 'token 12345'},
        })
        .reply(200);
      (api as any).request('method', '/path');
      requestHandler.done();
    });


    it('should reject on request error', async () => {
      nock('https://api.github.com')
        .intercept('/path', 'method')
        .replyWithError('Test');

      await expectAsync((api as any).request('method', '/path')).toBeRejectedWithError('Test');
    });


    it('should \'JSON.stringify\' and send the data along with the request', () => {
      const data = {key: 'value'};
      const requestHandler = nock('https://api.github.com')
        .intercept('/path', 'method', JSON.stringify(data))
        .reply(200);
      (api as any).request('method', '/path', data);
      requestHandler.done();
    });


    it('should reject if response statusCode is <200', async () => {
      const requestHandler = nock('https://api.github.com')
        .intercept('/path', 'method')
        .reply(199);
      const responsePromise = (api as any).request('method', '/path');

      await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('failed'));
      await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('status: 199'));

      requestHandler.done();
    });


    it('should reject if response statusCode is >=400', async () => {
      const requestHandler = nock('https://api.github.com')
        .intercept('/path', 'method')
        .reply(400);
      const responsePromise = (api as any).request('method', '/path');

      await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('failed'));
      await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('status: 400'));

      requestHandler.done();
    });


    it('should include the response text in the rejection message', async () => {
      const requestHandler = nock('https://api.github.com')
        .intercept('/path', 'method')
        .reply(500, 'Test');
      const responsePromise = (api as any).request('method', '/path');

      await expectAsync(responsePromise).toBeRejectedWith(jasmine.stringMatching('Test'));

      requestHandler.done();
     });


    it('should resolve if returned statusCode is >=200 and <400', async () => {
      const requestHandler = nock('https://api.github.com')
        .intercept('/path', 'method')
        .reply(200);

      await expectAsync((api as any).request('method', '/path')).toBeResolved();
      requestHandler.done();
    });


    it('should parse the response body into an object using \'JSON.parse\'', async () => {
      const requestHandler = nock('https://api.github.com')
        .intercept('/path', 'method')
        .reply(300, '{"foo": "bar"}');

      await expectAsync((api as any).request('method', '/path')).toBeResolvedTo({foo: 'bar'});
      requestHandler.done();
    });

    it('should reject if the response text is malformed JSON', async () => {
      const requestHandler = nock('https://api.github.com')
        .intercept('/path', 'method')
        .reply(300, '}');

      await expectAsync((api as any).request('method', '/path')).toBeRejectedWithError(SyntaxError);
      requestHandler.done();
    });

  });

});
