/**
 * @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 sinonChai from 'sinon-chai';

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

import { mockEndpoint } from '../../../test/helpers/api/helper';
import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
import * as mockFetch from '../../../test/helpers/mock_fetch';
import { Endpoint } from '../../api';
import { APIUserInfo } from '../../api/account_management/account';
import { ServerError } from '../../api/errors';
import { UserCredentialInternal } from '../../model/user';
import {
  applyActionCode,
  checkActionCode,
  confirmPasswordReset,
  createUserWithEmailAndPassword,
  sendPasswordResetEmail,
  signInWithEmailAndPassword,
  verifyPasswordResetCode
} from './email_and_password';

use(chaiAsPromised);
use(sinonChai);

describe('core/strategies/sendPasswordResetEmail', () => {
  const email = 'foo@bar.com';

  let auth: TestAuth;

  beforeEach(async () => {
    auth = await testAuth();
    mockFetch.setUp();
  });

  afterEach(mockFetch.tearDown);

  it('should send a password reset email', async () => {
    const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, {
      email
    });
    await sendPasswordResetEmail(auth, email);
    expect(mock.calls[0].request).to.eql({
      requestType: ActionCodeOperation.PASSWORD_RESET,
      email
    });
  });

  it('should surface errors', async () => {
    const mock = mockEndpoint(
      Endpoint.SEND_OOB_CODE,
      {
        error: {
          code: 400,
          message: ServerError.INVALID_EMAIL
        }
      },
      400
    );
    await expect(sendPasswordResetEmail(auth, email)).to.be.rejectedWith(
      FirebaseError,
      'Firebase: The email address is badly formatted. (auth/invalid-email).'
    );
    expect(mock.calls.length).to.eq(1);
  });

  context('on iOS', () => {
    it('should pass action code parameters', async () => {
      const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, {
        email
      });
      await sendPasswordResetEmail(auth, email, {
        handleCodeInApp: true,
        iOS: {
          bundleId: 'my-bundle'
        },
        url: 'my-url',
        dynamicLinkDomain: 'fdl-domain'
      });

      expect(mock.calls[0].request).to.eql({
        requestType: ActionCodeOperation.PASSWORD_RESET,
        email,
        continueUrl: 'my-url',
        dynamicLinkDomain: 'fdl-domain',
        canHandleCodeInApp: true,
        iosBundleId: 'my-bundle'
      });
    });
  });

  context('on Android', () => {
    it('should pass action code parameters', async () => {
      const mock = mockEndpoint(Endpoint.SEND_OOB_CODE, {
        email
      });
      await sendPasswordResetEmail(auth, email, {
        handleCodeInApp: true,
        android: {
          installApp: false,
          minimumVersion: 'my-version',
          packageName: 'my-package'
        },
        url: 'my-url',
        dynamicLinkDomain: 'fdl-domain'
      });
      expect(mock.calls[0].request).to.eql({
        requestType: ActionCodeOperation.PASSWORD_RESET,
        email,
        continueUrl: 'my-url',
        dynamicLinkDomain: 'fdl-domain',
        canHandleCodeInApp: true,
        androidInstallApp: false,
        androidMinimumVersionCode: 'my-version',
        androidPackageName: 'my-package'
      });
    });
  });
});

describe('core/strategies/confirmPasswordReset', () => {
  const oobCode = 'oob-code';
  const newPassword = 'new-password';

  let auth: TestAuth;

  beforeEach(async () => {
    auth = await testAuth();
    mockFetch.setUp();
  });

  afterEach(mockFetch.tearDown);

  it('should confirm the password reset and not return the email', async () => {
    const mock = mockEndpoint(Endpoint.RESET_PASSWORD, {
      email: 'foo@bar.com'
    });
    const response = await confirmPasswordReset(auth, oobCode, newPassword);
    expect(response).to.be.undefined;
    expect(mock.calls[0].request).to.eql({
      oobCode,
      newPassword
    });
  });

  it('should surface errors', async () => {
    const mock = mockEndpoint(
      Endpoint.RESET_PASSWORD,
      {
        error: {
          code: 400,
          message: ServerError.INVALID_OOB_CODE
        }
      },
      400
    );
    await expect(
      confirmPasswordReset(auth, oobCode, newPassword)
    ).to.be.rejectedWith(
      FirebaseError,
      'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).'
    );
    expect(mock.calls.length).to.eq(1);
  });
});

describe('core/strategies/applyActionCode', () => {
  const oobCode = 'oob-code';

  let auth: TestAuth;

  beforeEach(async () => {
    auth = await testAuth();
    mockFetch.setUp();
  });

  afterEach(mockFetch.tearDown);

  it('should apply the oob code', async () => {
    const mock = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
    await applyActionCode(auth, oobCode);
    expect(mock.calls[0].request).to.eql({
      oobCode
    });
  });

  it('should surface errors', async () => {
    const mock = mockEndpoint(
      Endpoint.SET_ACCOUNT_INFO,
      {
        error: {
          code: 400,
          message: ServerError.INVALID_OOB_CODE
        }
      },
      400
    );
    await expect(applyActionCode(auth, oobCode)).to.be.rejectedWith(
      FirebaseError,
      'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).'
    );
    expect(mock.calls.length).to.eq(1);
  });
});

describe('core/strategies/checkActionCode', () => {
  const oobCode = 'oob-code';
  const email = 'foo@bar.com';
  const newEmail = 'new@email.com';

  let auth: TestAuth;

  beforeEach(async () => {
    auth = await testAuth();
    mockFetch.setUp();
  });

  afterEach(mockFetch.tearDown);

  it('should verify the oob code', async () => {
    const mock = mockEndpoint(Endpoint.RESET_PASSWORD, {
      requestType: ActionCodeOperation.PASSWORD_RESET,
      email: 'foo@bar.com'
    });
    const response = await checkActionCode(auth, oobCode);
    expect(response).to.eql({
      data: {
        email,
        previousEmail: null,
        multiFactorInfo: null
      },
      operation: ActionCodeOperation.PASSWORD_RESET
    });
    expect(mock.calls[0].request).to.eql({
      oobCode
    });
  });

  it('should return the newEmail', async () => {
    const mock = mockEndpoint(Endpoint.RESET_PASSWORD, {
      requestType: ActionCodeOperation.PASSWORD_RESET,
      email,
      newEmail
    });
    const response = await checkActionCode(auth, oobCode);
    expect(response).to.eql({
      data: {
        email,
        previousEmail: newEmail,
        multiFactorInfo: null
      },
      operation: ActionCodeOperation.PASSWORD_RESET
    });
    expect(mock.calls[0].request).to.eql({
      oobCode
    });
  });

  it('should expect a requestType', async () => {
    const mock = mockEndpoint(Endpoint.RESET_PASSWORD, {
      email
    });
    await expect(checkActionCode(auth, oobCode)).to.be.rejectedWith(
      FirebaseError,
      'Firebase: An internal AuthError has occurred. (auth/internal-error).'
    );
    expect(mock.calls.length).to.eq(1);
  });

  it('should surface errors', async () => {
    const mock = mockEndpoint(
      Endpoint.RESET_PASSWORD,
      {
        error: {
          code: 400,
          message: ServerError.INVALID_OOB_CODE
        }
      },
      400
    );
    await expect(checkActionCode(auth, oobCode)).to.be.rejectedWith(
      FirebaseError,
      'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).'
    );
    expect(mock.calls.length).to.eq(1);
  });
});

describe('core/strategies/verifyPasswordResetCode', () => {
  const oobCode = 'oob-code';
  const email = 'foo@bar.com';

  let auth: TestAuth;

  beforeEach(async () => {
    auth = await testAuth();
    mockFetch.setUp();
  });

  afterEach(mockFetch.tearDown);

  it('should verify the oob code', async () => {
    const mock = mockEndpoint(Endpoint.RESET_PASSWORD, {
      requestType: ActionCodeOperation.PASSWORD_RESET,
      email: 'foo@bar.com',
      previousEmail: null
    });
    const response = await verifyPasswordResetCode(auth, oobCode);
    expect(response).to.eq(email);
    expect(mock.calls[0].request).to.eql({
      oobCode
    });
  });

  it('should expect a requestType', async () => {
    const mock = mockEndpoint(Endpoint.RESET_PASSWORD, {
      email
    });
    await expect(verifyPasswordResetCode(auth, oobCode)).to.be.rejectedWith(
      FirebaseError,
      'Firebase: An internal AuthError has occurred. (auth/internal-error).'
    );
    expect(mock.calls.length).to.eq(1);
  });

  it('should surface errors', async () => {
    const mock = mockEndpoint(
      Endpoint.RESET_PASSWORD,
      {
        error: {
          code: 400,
          message: ServerError.INVALID_OOB_CODE
        }
      },
      400
    );
    await expect(verifyPasswordResetCode(auth, oobCode)).to.be.rejectedWith(
      FirebaseError,
      'Firebase: The action code is invalid. This can happen if the code is malformed, expired, or has already been used. (auth/invalid-action-code).'
    );
    expect(mock.calls.length).to.eq(1);
  });
});

describe('core/strategies/email_and_password/createUserWithEmailAndPassword', () => {
  let auth: TestAuth;
  const serverUser: APIUserInfo = {
    localId: 'local-id'
  };

  beforeEach(async () => {
    auth = await testAuth();
    mockFetch.setUp();
    mockEndpoint(Endpoint.SIGN_UP, {
      idToken: 'id-token',
      refreshToken: 'refresh-token',
      expiresIn: '1234',
      localId: serverUser.localId!
    });
    mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
      users: [serverUser]
    });
  });
  afterEach(mockFetch.tearDown);

  it('should sign in the user', async () => {
    const {
      _tokenResponse,
      user,
      operationType
    } = (await createUserWithEmailAndPassword(
      auth,
      'some-email',
      'some-password'
    )) as UserCredentialInternal;
    expect(_tokenResponse).to.eql({
      idToken: 'id-token',
      refreshToken: 'refresh-token',
      expiresIn: '1234',
      localId: serverUser.localId!
    });
    expect(operationType).to.eq(OperationType.SIGN_IN);
    expect(user.uid).to.eq(serverUser.localId);
    expect(user.isAnonymous).to.be.false;
  });
});

describe('core/strategies/email_and_password/signInWithEmailAndPassword', () => {
  let auth: TestAuth;
  const serverUser: APIUserInfo = {
    localId: 'local-id'
  };

  beforeEach(async () => {
    auth = await testAuth();
    mockFetch.setUp();
    mockEndpoint(Endpoint.SIGN_IN_WITH_PASSWORD, {
      idToken: 'id-token',
      refreshToken: 'refresh-token',
      expiresIn: '1234',
      localId: serverUser.localId!
    });
    mockEndpoint(Endpoint.GET_ACCOUNT_INFO, {
      users: [serverUser]
    });
  });
  afterEach(mockFetch.tearDown);

  it('should sign in the user', async () => {
    const {
      _tokenResponse,
      user,
      operationType
    } = (await signInWithEmailAndPassword(
      auth,
      'some-email',
      'some-password'
    )) as UserCredentialInternal;
    expect(_tokenResponse).to.eql({
      idToken: 'id-token',
      refreshToken: 'refresh-token',
      expiresIn: '1234',
      localId: serverUser.localId!
    });
    expect(operationType).to.eq(OperationType.SIGN_IN);
    expect(user.uid).to.eq(serverUser.localId);
    expect(user.isAnonymous).to.be.false;
  });
});
