/**
 * @license
 * Copyright 2017 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import '../../test/common-test-setup';
import {Auth} from './gr-auth_impl';
import {getAppContext} from '../app-context';
import {stubBaseUrl} from '../../test/test-utils';
import {EventEmitterService} from '../gr-event-interface/gr-event-interface';
import {SinonFakeTimers} from 'sinon';
import {AuthRequestInit, DefaultAuthOptions} from './gr-auth';
import {assert} from '@open-wc/testing';

suite('gr-auth', () => {
  let auth: Auth;
  let eventEmitter: EventEmitterService;

  setup(() => {
    // TODO(poucet): Mock the eventEmitter completely instead of getting it
    // from appContext.
    eventEmitter = getAppContext().eventEmitter;
    auth = new Auth(eventEmitter);
  });

  suite('Auth class methods', () => {
    let fakeFetch: sinon.SinonStub;
    setup(() => {
      fakeFetch = sinon.stub(window, 'fetch');
    });

    test('auth-check returns 403', async () => {
      fakeFetch.returns(Promise.resolve({status: 403}));
      const authed = await auth.authCheck();
      assert.isFalse(authed);
      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
    });

    test('auth-check returns 204', async () => {
      fakeFetch.returns(Promise.resolve({status: 204}));
      const authed = await auth.authCheck();
      assert.isTrue(authed);
      assert.equal(auth.status, Auth.STATUS.AUTHED);
    });

    test('auth-check returns 502', async () => {
      fakeFetch.returns(Promise.resolve({status: 502}));
      const authed = await auth.authCheck();
      assert.isFalse(authed);
      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
    });

    test('auth-check failed', async () => {
      fakeFetch.returns(Promise.reject(new Error('random error')));
      const authed = await auth.authCheck();
      assert.isFalse(authed);
      assert.equal(auth.status, Auth.STATUS.ERROR);
    });
  });

  suite('cache and events behavior', () => {
    let fakeFetch: sinon.SinonStub;
    let clock: SinonFakeTimers;
    setup(() => {
      clock = sinon.useFakeTimers();
      fakeFetch = sinon.stub(window, 'fetch');
    });

    test('cache auth-check result', async () => {
      fakeFetch.returns(Promise.resolve({status: 403}));
      const authed = await auth.authCheck();
      assert.isFalse(authed);
      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
      fakeFetch.returns(Promise.resolve({status: 204}));
      const authed2 = await auth.authCheck();
      assert.isFalse(authed2);
      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
    });

    test('clearCache should refetch auth-check result', async () => {
      fakeFetch.returns(Promise.resolve({status: 403}));
      const authed = await auth.authCheck();
      assert.isFalse(authed);
      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
      fakeFetch.returns(Promise.resolve({status: 204}));
      auth.clearCache();
      const authed2 = await auth.authCheck();
      assert.isTrue(authed2);
      assert.equal(auth.status, Auth.STATUS.AUTHED);
    });

    test('cache expired on auth-check after certain time', async () => {
      fakeFetch.returns(Promise.resolve({status: 403}));
      const authed = await auth.authCheck();
      assert.isFalse(authed);
      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
      clock.tick(1000 * 10000);
      fakeFetch.returns(Promise.resolve({status: 204}));
      const authed2 = await auth.authCheck();
      assert.isTrue(authed2);
      assert.equal(auth.status, Auth.STATUS.AUTHED);
    });

    test('no cache if auth-check failed', async () => {
      fakeFetch.returns(Promise.reject(new Error('random error')));
      const authed = await auth.authCheck();
      assert.isFalse(authed);
      assert.equal(auth.status, Auth.STATUS.ERROR);
      assert.equal(fakeFetch.callCount, 1);
      await auth.authCheck();
      assert.equal(fakeFetch.callCount, 2);
    });

    test('fire event when switch from authed to unauthed', async () => {
      fakeFetch.returns(Promise.resolve({status: 204}));
      const authed = await auth.authCheck();
      assert.isTrue(authed);
      assert.equal(auth.status, Auth.STATUS.AUTHED);
      clock.tick(1000 * 10000);
      fakeFetch.returns(Promise.resolve({status: 403}));
      const emitStub = sinon.stub(eventEmitter, 'emit');
      const authed2 = await auth.authCheck();
      assert.isFalse(authed2);
      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
      assert.isTrue(emitStub.called);
    });

    test('fire event when switch from authed to error', async () => {
      fakeFetch.returns(Promise.resolve({status: 204}));
      const authed = await auth.authCheck();
      assert.isTrue(authed);
      assert.equal(auth.status, Auth.STATUS.AUTHED);
      clock.tick(1000 * 10000);
      fakeFetch.returns(Promise.reject(new Error('random error')));
      const emitStub = sinon.stub(eventEmitter, 'emit');
      const authed2 = await auth.authCheck();
      assert.isFalse(authed2);
      assert.isTrue(emitStub.called);
      assert.equal(auth.status, Auth.STATUS.ERROR);
    });

    test('no event from non-authed to other status', async () => {
      fakeFetch.returns(Promise.resolve({status: 403}));
      const authed = await auth.authCheck();
      assert.isFalse(authed);
      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
      clock.tick(1000 * 10000);
      fakeFetch.returns(Promise.resolve({status: 204}));
      const emitStub = sinon.stub(eventEmitter, 'emit');
      const authed2 = await auth.authCheck();
      assert.isTrue(authed2);
      assert.isFalse(emitStub.called);
      assert.equal(auth.status, Auth.STATUS.AUTHED);
    });

    test('no event from non-authed to other status', async () => {
      fakeFetch.returns(Promise.resolve({status: 403}));
      const authed = await auth.authCheck();
      assert.isFalse(authed);
      assert.equal(auth.status, Auth.STATUS.NOT_AUTHED);
      clock.tick(1000 * 10000);
      fakeFetch.returns(Promise.reject(new Error('random error')));
      const emitStub = sinon.stub(eventEmitter, 'emit');
      const authed2 = await auth.authCheck();
      assert.isFalse(authed2);
      assert.isFalse(emitStub.called);
      assert.equal(auth.status, Auth.STATUS.ERROR);
    });
  });

  suite('default (xsrf token header)', () => {
    let fakeFetch: sinon.SinonStub;

    setup(() => {
      fakeFetch = sinon
        .stub(window, 'fetch')
        .returns(Promise.resolve({...new Response(), ok: true}));
    });

    test('GET', async () => {
      await auth.fetch('/url', {bar: 'bar'} as AuthRequestInit);
      const [url, options] = fakeFetch.lastCall.args;
      assert.equal(url, '/url');
      assert.equal(options.credentials, 'same-origin');
    });

    test('POST', async () => {
      sinon.stub(auth, '_getCookie').withArgs('XSRF_TOKEN').returns('foobar');
      await auth.fetch('/url', {method: 'POST'});
      const [url, options] = fakeFetch.lastCall.args;
      assert.equal(url, '/url');
      assert.equal(options.credentials, 'same-origin');
      assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar');
    });
  });

  suite('cors (access token)', () => {
    let fakeFetch: sinon.SinonStub;

    setup(() => {
      fakeFetch = sinon
        .stub(window, 'fetch')
        .returns(Promise.resolve({...new Response(), ok: true}));
    });

    let getToken: sinon.SinonStub;

    const makeToken = (opt_accessToken?: string) => {
      return {
        access_token: opt_accessToken || 'zbaz',
        expires_at: new Date(Date.now() + 10e8).getTime(),
      };
    };

    setup(() => {
      getToken = sinon.stub();
      getToken.returns(Promise.resolve(makeToken()));
      const defaultOptions: DefaultAuthOptions = {
        credentials: 'include',
      };
      auth.setup(getToken, defaultOptions);
    });

    test('base url support', async () => {
      const baseUrl = 'http://foo';
      stubBaseUrl(baseUrl);
      await auth.fetch(baseUrl + '/url', {bar: 'bar'} as AuthRequestInit);
      const [url] = fakeFetch.lastCall.args;
      assert.equal(url, 'http://foo/a/url?access_token=zbaz');
    });

    test('fetch not signed in', async () => {
      getToken.returns(Promise.resolve());
      await auth.fetch('/url', {bar: 'bar'} as AuthRequestInit);
      const [url, options] = fakeFetch.lastCall.args;
      assert.equal(url, '/url');
      assert.equal(options.bar, 'bar');
      assert.equal(Object.keys(options.headers).length, 0);
    });

    test('fetch signed in', async () => {
      await auth.fetch('/url', {bar: 'bar'} as AuthRequestInit);
      const [url, options] = fakeFetch.lastCall.args;
      assert.equal(url, '/a/url?access_token=zbaz');
      assert.equal(options.bar, 'bar');
    });

    test('getToken calls are cached', async () => {
      await Promise.all([auth.fetch('/url-one'), auth.fetch('/url-two')]);
      assert.equal(getToken.callCount, 1);
    });

    test('getToken refreshes token', async () => {
      const isTokenValidStub = sinon.stub(auth, '_isTokenValid');
      isTokenValidStub
        .onFirstCall()
        .returns(true)
        .onSecondCall()
        .returns(false)
        .onThirdCall()
        .returns(true);
      await auth.fetch('/url-one');
      getToken.returns(Promise.resolve(makeToken('bzzbb')));
      await auth.fetch('/url-two');

      const [[firstUrl], [secondUrl]] = fakeFetch.args;
      assert.equal(firstUrl, '/a/url-one?access_token=zbaz');
      assert.equal(secondUrl, '/a/url-two?access_token=bzzbb');
    });

    test('signed in token error falls back to anonymous', async () => {
      getToken.returns(Promise.resolve('rubbish'));
      await auth.fetch('/url', {bar: 'bar'} as AuthRequestInit);
      const [url, options] = fakeFetch.lastCall.args;
      assert.equal(url, '/url');
      assert.equal(options.bar, 'bar');
    });

    test('_isTokenValid', () => {
      assert.isFalse(auth._isTokenValid(null));
      assert.isFalse(auth._isTokenValid({}));
      assert.isFalse(auth._isTokenValid({access_token: 'foo'}));
      assert.isFalse(
        auth._isTokenValid({
          access_token: 'foo',
          expires_at: `${Date.now() / 1000 - 1}`,
        })
      );
      assert.isTrue(
        auth._isTokenValid({
          access_token: 'foo',
          expires_at: `${Date.now() / 1000 + 1}`,
        })
      );
    });

    test('HTTP PUT with content type', async () => {
      const originalOptions = {
        method: 'PUT',
        headers: new Headers({'Content-Type': 'mail/pigeon'}),
      };
      await auth.fetch('/url', originalOptions);
      assert.isTrue(getToken.called);
      const [url, options] = fakeFetch.lastCall.args;
      assert.include(url, '$ct=mail%2Fpigeon');
      assert.include(url, '$m=PUT');
      assert.include(url, 'access_token=zbaz');
      assert.equal(options.method, 'POST');
      assert.equal(options.headers.get('Content-Type'), 'text/plain');
    });

    test('HTTP PUT without content type', async () => {
      const originalOptions = {
        method: 'PUT',
      };
      await auth.fetch('/url', originalOptions);
      assert.isTrue(getToken.called);
      const [url, options] = fakeFetch.lastCall.args;
      assert.include(url, '$ct=text%2Fplain');
      assert.include(url, '$m=PUT');
      assert.include(url, 'access_token=zbaz');
      assert.equal(options.method, 'POST');
      assert.equal(options.headers.get('Content-Type'), 'text/plain');
    });
  });
});
