/**
 * @license
 * Copyright 2020 Google LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { expect, use } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import * as sinon from 'sinon';
import * as sinonChai from 'sinon-chai';

import {
  OperationType,
  PopupRedirectResolver,
  ProviderId
} from '../../model/public_types';
import { FirebaseError } from '@firebase/util';

import { delay } from '../../../test/helpers/delay';
import { BASE_AUTH_EVENT } from '../../../test/helpers/iframe_event';
import { testAuth, testUser, TestAuth } from '../../../test/helpers/mock_auth';
import { makeMockPopupRedirectResolver } from '../../../test/helpers/mock_popup_redirect_resolver';
import { stubTimeouts, TimerMap } from '../../../test/helpers/timeout_stub';
import { AuthEvent, AuthEventType } from '../../model/popup_redirect';
import { UserInternal } from '../../model/user';
import { AuthEventManager } from '../../core/auth/auth_event_manager';
import { AuthErrorCode } from '../../core/errors';
import { OAuthProvider } from '../../core/providers/oauth';
import { UserCredentialImpl } from '../../core/user/user_credential_impl';
import * as eid from '../../core/util/event_id';
import { AuthPopup } from '../util/popup';
import * as idpTasks from '../../core/strategies/idp';
import {
  _Timeout,
  _POLL_WINDOW_CLOSE_TIMEOUT,
  linkWithPopup,
  reauthenticateWithPopup,
  signInWithPopup
} from './popup';
import { _getInstance } from '../../../internal';
import { _createError } from '../../core/util/assert';

use(sinonChai);
use(chaiAsPromised);

const MATCHING_EVENT_ID = 'matching-event-id';
const OTHER_EVENT_ID = 'wrong-id';

describe('platform_browser/strategies/popup', () => {
  let resolver: PopupRedirectResolver;
  let provider: OAuthProvider;
  let eventManager: AuthEventManager;
  let authPopup: AuthPopup;
  let underlyingWindow: { closed: boolean };
  let auth: TestAuth;
  let idpStubs: sinon.SinonStubbedInstance<typeof idpTasks>;
  let pendingTimeouts: TimerMap;

  beforeEach(async () => {
    auth = await testAuth();
    eventManager = new AuthEventManager(auth);
    underlyingWindow = { closed: false };
    authPopup = new AuthPopup(underlyingWindow as Window);
    provider = new OAuthProvider(ProviderId.GOOGLE);
    resolver = makeMockPopupRedirectResolver(eventManager, authPopup);
    idpStubs = sinon.stub(idpTasks);
    sinon.stub(eid, '_generateEventId').returns(MATCHING_EVENT_ID);
    pendingTimeouts = stubTimeouts();
    sinon.stub(window, 'clearTimeout');
  });

  afterEach(() => {
    sinon.restore();
  });

  function iframeEvent(event: Partial<AuthEvent>): void {
    // Push the dispatch out of the synchronous flow
    delay(() => {
      eventManager.onEvent({
        ...BASE_AUTH_EVENT,
        eventId: MATCHING_EVENT_ID,
        ...event
      });
    });
  }

  context('signInWithPopup', () => {
    it('completes the full flow', async () => {
      const cred = new UserCredentialImpl({
        user: testUser(auth, 'uid'),
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.SIGN_IN
      });
      idpStubs._signIn.returns(Promise.resolve(cred));
      const promise = signInWithPopup(auth, provider, resolver);
      iframeEvent({
        type: AuthEventType.SIGN_IN_VIA_POPUP
      });
      expect(await promise).to.eq(cred);
    });

    it('completes the full flow with default resolver', async () => {
      const cred = new UserCredentialImpl({
        user: testUser(auth, 'uid'),
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.SIGN_IN
      });
      auth._popupRedirectResolver = _getInstance(resolver);
      idpStubs._signIn.returns(Promise.resolve(cred));
      const promise = signInWithPopup(auth, provider);
      iframeEvent({
        type: AuthEventType.SIGN_IN_VIA_POPUP
      });
      expect(await promise).to.eq(cred);
    });

    it('errors if resolver not provided and not on auth', async () => {
      const cred = new UserCredentialImpl({
        user: testUser(auth, 'uid'),
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.SIGN_IN
      });
      idpStubs._signIn.returns(Promise.resolve(cred));
      await expect(signInWithPopup(auth, provider)).to.be.rejectedWith(
        FirebaseError,
        'auth/argument-error'
      );
    });

    it('ignores events for another event id', async () => {
      const cred = new UserCredentialImpl({
        user: testUser(auth, 'uid'),
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.SIGN_IN
      });
      idpStubs._signIn.returns(Promise.resolve(cred));
      const promise = signInWithPopup(auth, provider, resolver);
      iframeEvent({
        type: AuthEventType.SIGN_IN_VIA_POPUP,
        eventId: OTHER_EVENT_ID,
        error: {
          code: 'auth/internal-error',
          message: '',
          name: ''
        }
      });

      iframeEvent({
        type: AuthEventType.SIGN_IN_VIA_POPUP,
        eventId: MATCHING_EVENT_ID
      });
      expect(await promise).to.eq(cred);
    });

    it('does not call idp tasks if event is error', async () => {
      const cred = new UserCredentialImpl({
        user: testUser(auth, 'uid'),
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.SIGN_IN
      });
      idpStubs._signIn.returns(Promise.resolve(cred));
      const promise = signInWithPopup(auth, provider, resolver);
      iframeEvent({
        type: AuthEventType.SIGN_IN_VIA_POPUP,
        eventId: MATCHING_EVENT_ID,
        error: {
          code: 'auth/invalid-app-credential',
          message: '',
          name: ''
        }
      });
      await expect(promise).to.be.rejectedWith(
        FirebaseError,
        'auth/invalid-app-credential'
      );
      expect(idpStubs._signIn).not.to.have.been.called;
    });

    it('does not error if the poll timeout trips', async () => {
      const cred = new UserCredentialImpl({
        user: testUser(auth, 'uid'),
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.SIGN_IN
      });
      idpStubs._signIn.returns(Promise.resolve(cred));
      const promise = signInWithPopup(auth, provider, resolver);
      delay(() => {
        underlyingWindow.closed = true;
        pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()]();
      });
      iframeEvent({
        type: AuthEventType.SIGN_IN_VIA_POPUP
      });
      expect(await promise).to.eq(cred);
    });

    it('does error if the poll timeout and event timeout trip', async () => {
      const cred = new UserCredentialImpl({
        user: testUser(auth, 'uid'),
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.SIGN_IN
      });
      idpStubs._signIn.returns(Promise.resolve(cred));
      const promise = signInWithPopup(auth, provider, resolver);
      delay(() => {
        underlyingWindow.closed = true;
        pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()]();
        pendingTimeouts[_Timeout.AUTH_EVENT]();
      });
      iframeEvent({
        type: AuthEventType.SIGN_IN_VIA_POPUP
      });
      await expect(promise).to.be.rejectedWith(
        FirebaseError,
        'auth/popup-closed-by-user'
      );
    });

    it('errors if webstorage support comes back negative', async () => {
      resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false);
      await expect(
        signInWithPopup(auth, provider, resolver)
      ).to.be.rejectedWith(FirebaseError, 'auth/web-storage-unsupported');
    });

    it('passes any errors from idp task', async () => {
      idpStubs._signIn.returns(
        Promise.reject(_createError(auth, AuthErrorCode.INVALID_APP_ID))
      );
      const promise = signInWithPopup(auth, provider, resolver);
      iframeEvent({
        eventId: MATCHING_EVENT_ID,
        type: AuthEventType.SIGN_IN_VIA_POPUP
      });

      await expect(promise).to.be.rejectedWith(
        FirebaseError,
        'auth/invalid-app-id'
      );
    });

    it('cancels the task if called consecutively', async () => {
      const cred = new UserCredentialImpl({
        user: testUser(auth, 'uid'),
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.SIGN_IN
      });
      idpStubs._signIn.returns(Promise.resolve(cred));
      const firstPromise = signInWithPopup(auth, provider, resolver);
      const secondPromise = signInWithPopup(auth, provider, resolver);
      iframeEvent({
        type: AuthEventType.SIGN_IN_VIA_POPUP
      });
      await expect(firstPromise).to.be.rejectedWith(
        FirebaseError,
        'auth/cancelled-popup-request'
      );
      expect(await secondPromise).to.eq(cred);
    });
  });

  context('linkWithPopup', () => {
    let user: UserInternal;
    beforeEach(() => {
      user = testUser(auth, 'uid');
    });

    it('completes the full flow', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.LINK
      });
      idpStubs._link.returns(Promise.resolve(cred));
      const promise = linkWithPopup(user, provider, resolver);
      iframeEvent({
        type: AuthEventType.LINK_VIA_POPUP
      });
      expect(await promise).to.eq(cred);
    });

    it('completes the full flow with default resolver', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.LINK
      });
      user.auth._popupRedirectResolver = _getInstance(resolver);
      idpStubs._link.returns(Promise.resolve(cred));
      const promise = linkWithPopup(user, provider);
      iframeEvent({
        type: AuthEventType.LINK_VIA_POPUP
      });
      expect(await promise).to.eq(cred);
    });

    it('errors if resolver not provided and not on auth', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.LINK
      });
      idpStubs._link.returns(Promise.resolve(cred));
      await expect(linkWithPopup(user, provider)).to.be.rejectedWith(
        FirebaseError,
        'auth/argument-error'
      );
    });

    it('ignores events for another event id', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.LINK
      });
      idpStubs._link.returns(Promise.resolve(cred));
      const promise = linkWithPopup(user, provider, resolver);
      iframeEvent({
        type: AuthEventType.LINK_VIA_POPUP,
        eventId: OTHER_EVENT_ID,
        error: {
          code: 'auth/internal-error',
          message: '',
          name: ''
        }
      });

      iframeEvent({
        type: AuthEventType.LINK_VIA_POPUP,
        eventId: MATCHING_EVENT_ID
      });
      expect(await promise).to.eq(cred);
    });

    it('does not call idp tasks if event is error', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.LINK
      });
      idpStubs._link.returns(Promise.resolve(cred));
      const promise = linkWithPopup(user, provider, resolver);
      iframeEvent({
        type: AuthEventType.LINK_VIA_POPUP,
        eventId: MATCHING_EVENT_ID,
        error: {
          code: 'auth/invalid-app-credential',
          message: '',
          name: ''
        }
      });
      await expect(promise).to.be.rejectedWith(
        FirebaseError,
        'auth/invalid-app-credential'
      );
      expect(idpStubs._link).not.to.have.been.called;
    });

    it('does not error if the poll timeout trips', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.LINK
      });
      idpStubs._link.returns(Promise.resolve(cred));
      const promise = linkWithPopup(user, provider, resolver);
      delay(() => {
        underlyingWindow.closed = true;
        pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()]();
      });
      iframeEvent({
        type: AuthEventType.LINK_VIA_POPUP
      });
      expect(await promise).to.eq(cred);
    });

    it('does error if the poll timeout and event timeout trip', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.LINK
      });
      idpStubs._link.returns(Promise.resolve(cred));
      const promise = linkWithPopup(user, provider, resolver);
      delay(() => {
        underlyingWindow.closed = true;
        pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()]();
        pendingTimeouts[_Timeout.AUTH_EVENT]();
      });
      iframeEvent({
        type: AuthEventType.LINK_VIA_POPUP
      });
      await expect(promise).to.be.rejectedWith(
        FirebaseError,
        'auth/popup-closed-by-user'
      );
    });

    it('errors if webstorage support comes back negative', async () => {
      resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false);
      await expect(linkWithPopup(user, provider, resolver)).to.be.rejectedWith(
        FirebaseError,
        'auth/web-storage-unsupported'
      );
    });

    it('passes any errors from idp task', async () => {
      idpStubs._link.returns(
        Promise.reject(_createError(auth, AuthErrorCode.INVALID_APP_ID))
      );
      const promise = linkWithPopup(user, provider, resolver);
      iframeEvent({
        eventId: MATCHING_EVENT_ID,
        type: AuthEventType.LINK_VIA_POPUP
      });

      await expect(promise).to.be.rejectedWith(
        FirebaseError,
        'auth/invalid-app-id'
      );
    });

    it('cancels the task if called consecutively', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.LINK
      });
      idpStubs._link.returns(Promise.resolve(cred));
      const firstPromise = linkWithPopup(user, provider, resolver);
      const secondPromise = linkWithPopup(user, provider, resolver);
      iframeEvent({
        type: AuthEventType.LINK_VIA_POPUP
      });
      await expect(firstPromise).to.be.rejectedWith(
        FirebaseError,
        'auth/cancelled-popup-request'
      );
      expect(await secondPromise).to.eq(cred);
    });
  });

  context('reauthenticateWithPopup', () => {
    let user: UserInternal;
    beforeEach(() => {
      user = testUser(auth, 'uid');
    });

    it('completes the full flow', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.REAUTHENTICATE
      });
      idpStubs._reauth.returns(Promise.resolve(cred));
      const promise = reauthenticateWithPopup(user, provider, resolver);
      iframeEvent({
        type: AuthEventType.REAUTH_VIA_POPUP
      });
      expect(await promise).to.eq(cred);
    });

    it('completes the full flow with default resolver', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.REAUTHENTICATE
      });
      user.auth._popupRedirectResolver = _getInstance(resolver);
      idpStubs._reauth.returns(Promise.resolve(cred));
      const promise = reauthenticateWithPopup(user, provider);
      iframeEvent({
        type: AuthEventType.REAUTH_VIA_POPUP
      });
      expect(await promise).to.eq(cred);
    });

    it('errors if resolver not provided and not on auth', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.REAUTHENTICATE
      });
      idpStubs._reauth.returns(Promise.resolve(cred));
      await expect(reauthenticateWithPopup(user, provider)).to.be.rejectedWith(
        FirebaseError,
        'auth/argument-error'
      );
    });

    it('ignores events for another event id', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.REAUTHENTICATE
      });
      idpStubs._reauth.returns(Promise.resolve(cred));
      const promise = reauthenticateWithPopup(user, provider, resolver);
      iframeEvent({
        type: AuthEventType.REAUTH_VIA_POPUP,
        eventId: OTHER_EVENT_ID,
        error: {
          code: 'auth/internal-error',
          message: '',
          name: ''
        }
      });

      iframeEvent({
        type: AuthEventType.REAUTH_VIA_POPUP,
        eventId: MATCHING_EVENT_ID
      });
      expect(await promise).to.eq(cred);
    });

    it('does not call idp tasks if event is error', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.REAUTHENTICATE
      });
      idpStubs._reauth.returns(Promise.resolve(cred));
      const promise = reauthenticateWithPopup(user, provider, resolver);
      iframeEvent({
        type: AuthEventType.REAUTH_VIA_POPUP,
        eventId: MATCHING_EVENT_ID,
        error: {
          code: 'auth/invalid-app-credential',
          message: '',
          name: ''
        }
      });
      await expect(promise).to.be.rejectedWith(
        FirebaseError,
        'auth/invalid-app-credential'
      );
      expect(idpStubs._reauth).not.to.have.been.called;
    });

    it('does not error if the poll timeout trips', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.REAUTHENTICATE
      });
      idpStubs._reauth.returns(Promise.resolve(cred));
      const promise = reauthenticateWithPopup(user, provider, resolver);
      delay(() => {
        underlyingWindow.closed = true;
        pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()]();
      });
      iframeEvent({
        type: AuthEventType.REAUTH_VIA_POPUP
      });
      expect(await promise).to.eq(cred);
    });

    it('errors if webstorage support comes back negative', async () => {
      resolver = makeMockPopupRedirectResolver(eventManager, authPopup, false);
      await expect(
        reauthenticateWithPopup(user, provider, resolver)
      ).to.be.rejectedWith(FirebaseError, 'auth/web-storage-unsupported');
    });

    it('does error if the poll timeout and event timeout trip', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.REAUTHENTICATE
      });
      idpStubs._reauth.returns(Promise.resolve(cred));
      const promise = reauthenticateWithPopup(user, provider, resolver);
      delay(() => {
        underlyingWindow.closed = true;
        pendingTimeouts[_POLL_WINDOW_CLOSE_TIMEOUT.get()]();
        pendingTimeouts[_Timeout.AUTH_EVENT]();
      });
      iframeEvent({
        type: AuthEventType.REAUTH_VIA_POPUP
      });
      await expect(promise).to.be.rejectedWith(
        FirebaseError,
        'auth/popup-closed-by-user'
      );
    });

    it('passes any errors from idp task', async () => {
      idpStubs._reauth.returns(
        Promise.reject(_createError(auth, AuthErrorCode.INVALID_APP_ID))
      );
      const promise = reauthenticateWithPopup(user, provider, resolver);
      iframeEvent({
        eventId: MATCHING_EVENT_ID,
        type: AuthEventType.REAUTH_VIA_POPUP
      });

      await expect(promise).to.be.rejectedWith(
        FirebaseError,
        'auth/invalid-app-id'
      );
    });

    it('cancels the task if called consecutively', async () => {
      const cred = new UserCredentialImpl({
        user,
        providerId: ProviderId.GOOGLE,
        operationType: OperationType.REAUTHENTICATE
      });
      idpStubs._reauth.returns(Promise.resolve(cred));
      const firstPromise = reauthenticateWithPopup(user, provider, resolver);
      const secondPromise = reauthenticateWithPopup(user, provider, resolver);
      iframeEvent({
        type: AuthEventType.REAUTH_VIA_POPUP
      });
      await expect(firstPromise).to.be.rejectedWith(
        FirebaseError,
        'auth/cancelled-popup-request'
      );
      expect(await secondPromise).to.eq(cred);
    });
  });
});
