/**
 * @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 { stub } from 'sinon';

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

import { mockEndpoint } from '../../../test/helpers/api/helper';
import { makeJWT } from '../../../test/helpers/jwt';
import { testAuth, testUser, TestAuth } from '../../../test/helpers/mock_auth';
import { MockAuthCredential } from '../../../test/helpers/mock_auth_credential';
import * as mockFetch from '../../../test/helpers/mock_fetch';
import { Endpoint } from '../../api';
import { APIUserInfo } from '../../api/account_management/account';
import { IdTokenMfaResponse } from '../../api/authentication/mfa';
import { MultiFactorError } from '../../mfa/mfa_error';
import { IdTokenResponse, IdTokenResponseKind } from '../../model/id_token';
import { UserInternal, UserCredentialInternal } from '../../model/user';
import { AuthCredential } from '../credentials';
import { AuthErrorCode } from '../errors';
import {
  linkWithCredential,
  reauthenticateWithCredential,
  signInWithCredential,
  _signInWithCredential
} from './credential';
import { _createError } from '../util/assert';

use(chaiAsPromised);

describe('core/strategies/credential', () => {
  const serverUser: APIUserInfo = {
    localId: 'local-id',
    displayName: 'display-name',
    photoUrl: 'photo-url',
    email: 'email',
    emailVerified: true,
    phoneNumber: 'phone-number',
    createdAt: 123,
    lastLoginAt: 456
  };

  const idTokenResponse: IdTokenResponse = {
    idToken: 'my-id-token',
    refreshToken: 'my-refresh-token',
    expiresIn: '1234',
    localId: serverUser.localId!,
    kind: IdTokenResponseKind.CreateAuthUri
  };

  let authCredential: AuthCredential;
  let auth: TestAuth;
  let getAccountInfoEndpoint: mockFetch.Route;
  let user: UserInternal;

  beforeEach(async () => {
    auth = await testAuth();
    mockFetch.setUp();
    authCredential = new MockAuthCredential(
      ProviderId.FIREBASE,
      SignInMethod.EMAIL_LINK
    );
    getAccountInfoEndpoint = mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
      users: [serverUser]
    });

    user = testUser(auth, 'uid', undefined, true);
  });

  afterEach(mockFetch.tearDown);

  describe('signInWithCredential', () => {
    it('should return a valid user credential', async () => {
      stub(authCredential, '_getIdTokenResponse').returns(
        Promise.resolve(idTokenResponse)
      );
      const { user, operationType, ...rest } = await signInWithCredential(
        auth,
        authCredential
      );
      expect((rest as UserCredentialInternal)._tokenResponse).to.eq(
        idTokenResponse
      );
      expect(user.uid).to.eq('local-id');
      expect(user.displayName).to.eq('display-name');
      expect(operationType).to.eq(OperationType.SIGN_IN);
    });

    it('should update the current user', async () => {
      stub(authCredential, '_getIdTokenResponse').returns(
        Promise.resolve(idTokenResponse)
      );
      const { user } = await signInWithCredential(auth, authCredential);
      expect(auth.currentUser).to.eq(user);
    });

    it('does not update the current user if bypass is true', async () => {
      stub(authCredential, '_getIdTokenResponse').returns(
        Promise.resolve(idTokenResponse)
      );
      const { user } = await _signInWithCredential(auth, authCredential, true);
      expect(auth.currentUser).to.be.null;
      expect(user).not.to.be.null;
    });

    it('should handle MFA', async () => {
      const serverResponse: IdTokenMfaResponse = {
        localId: 'uid',
        mfaInfo: [
          {
            mfaEnrollmentId: 'mfa-enrollment-id',
            enrolledAt: Date.now(),
            phoneInfo: 'phone-info'
          }
        ],
        mfaPendingCredential: 'mfa-pending-credential'
      };
      stub(authCredential, '_getIdTokenResponse').returns(
        Promise.reject(
          _createError(auth, AuthErrorCode.MFA_REQUIRED, {
            serverResponse
          })
        )
      );
      const error = await expect(
        signInWithCredential(auth, authCredential)
      ).to.be.rejectedWith(MultiFactorError);
      expect(error.operationType).to.eq(OperationType.SIGN_IN);
      expect(error.serverResponse).to.eql(serverResponse);
      expect(error.user).to.be.undefined;
    });
  });

  describe('reauthenticateWithCredential', () => {
    it('should throw an error if the uid is mismatched', async () => {
      stub(authCredential, '_getReauthenticationResolver').returns(
        Promise.resolve({
          ...idTokenResponse,
          idToken: makeJWT({ sub: 'not-my-uid' })
        })
      );

      await expect(
        reauthenticateWithCredential(user, authCredential)
      ).to.be.rejectedWith(
        FirebaseError,
        'Firebase: The supplied credentials do not correspond to the previously signed in user. (auth/user-mismatch).'
      );
    });

    it('should return the expected user credential', async () => {
      stub(authCredential, '_getReauthenticationResolver').returns(
        Promise.resolve({
          ...idTokenResponse,
          idToken: makeJWT({ sub: 'uid' })
        })
      );

      const {
        user: newUser,
        operationType,
        ...rest
      } = await reauthenticateWithCredential(user, authCredential);
      expect(operationType).to.eq(OperationType.REAUTHENTICATE);
      expect(newUser).to.eq(user);
      expect((rest as UserCredentialInternal)._tokenResponse).to.eql({
        ...idTokenResponse,
        idToken: makeJWT({ sub: 'uid' })
      });
    });
  });

  describe('linkWithCredential', () => {
    it('should throw an error if the provider is already linked', async () => {
      stub(authCredential, '_linkToIdToken').returns(
        Promise.resolve(idTokenResponse)
      );
      getAccountInfoEndpoint.response = {
        users: [
          {
            ...serverUser,
            providerUserInfo: [{ providerId: ProviderId.FIREBASE }]
          }
        ]
      };

      await expect(linkWithCredential(user, authCredential)).to.be.rejectedWith(
        FirebaseError,
        'Firebase: User can only be linked to one identity for the given provider. (auth/provider-already-linked).'
      );
    });

    it('should return a valid user credential', async () => {
      stub(authCredential, '_linkToIdToken').returns(
        Promise.resolve(idTokenResponse)
      );
      const {
        user: newUser,
        operationType,
        ...rest
      } = await linkWithCredential(user, authCredential);
      expect(operationType).to.eq(OperationType.LINK);
      expect(newUser).to.eq(user);
      expect((rest as UserCredentialInternal)._tokenResponse).to.eq(
        idTokenResponse
      );
    });
  });
});
