/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {AeadKeyTemplates} from '../aead/aead_key_templates';
import {PbEciesAeadDemParams, PbEciesAeadHkdfParams, PbEciesAeadHkdfPrivateKey, PbEciesAeadHkdfPublicKey, PbEciesHkdfKemParams, PbEllipticCurveType, PbHashType, PbKeyTemplate, PbPointFormat} from '../internal/proto';
import {bytesAsU8} from '../internal/proto_shims';
import * as Util from '../internal/util';
import * as Bytes from '../subtle/bytes';
import * as EllipticCurves from '../subtle/elliptic_curves';
import {assertExists} from '../testing/internal/test_utils';

import * as EciesAeadHkdfUtil from './ecies_aead_hkdf_util';

describe('ecies aead hkdf util test', function() {
  beforeEach(function() {
    // Use a generous promise timeout for running continuously.
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 1000;  // 1000s
  });

  afterEach(function() {
    // Reset the promise timeout to default value.
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;  // 1s
  });

  it('get json web key from proto, public key', async function() {
    for (const curve
             of [PbEllipticCurveType.NIST_P256,
                 PbEllipticCurveType.NIST_P384,
                 PbEllipticCurveType.NIST_P521,
    ]) {
      const key = await createKey(curve);
      const jwk = EciesAeadHkdfUtil.getJsonWebKeyFromProto(
          assertExists(key.getPublicKey()));

      // Test the returned jwk.
      const curveTypeSubtle = Util.curveTypeProtoToSubtle(curve);
      const curveTypeString = EllipticCurves.curveToString(curveTypeSubtle);
      expect(jwk?.['kty']).toBe('EC');
      expect(jwk?.['crv']).toBe(curveTypeString);
      expect(Bytes.fromBase64(assertExists(jwk['x']), /* opt_webSafe = */ true))
          .toEqual(bytesAsU8(assertExists(key.getPublicKey()).getX()));
      expect(Bytes.fromBase64(assertExists(jwk['y']), /* opt_webSafe = */ true))
          .toEqual(bytesAsU8(assertExists(key.getPublicKey()).getY()));
      expect(jwk?.['d']).toEqual(undefined);
      expect(jwk?.['ext']).toBe(true);
    }
  });

  it('get json web key from proto, public key, with leading zeros',
     async function() {
       for (const curve
                of [PbEllipticCurveType.NIST_P256,
                    PbEllipticCurveType.NIST_P384,
                    PbEllipticCurveType.NIST_P521,
       ]) {
         const key = await createKey(curve);

         // Add leading zeros to x and y value of key.
         const x = bytesAsU8(assertExists(key.getPublicKey()).getX());
         const y = bytesAsU8(assertExists(key.getPublicKey()).getY());
         key.getPublicKey()?.setX(
             Bytes.concat(new Uint8Array([0, 0, 0, 0, 0]), x));
         key.getPublicKey()?.setY(Bytes.concat(new Uint8Array([0, 0, 0]), y));
         const jwk = EciesAeadHkdfUtil.getJsonWebKeyFromProto(
             assertExists(key.getPublicKey()));

         // Test the returned jwk.
         const curveTypeSubtle = Util.curveTypeProtoToSubtle(curve);
         const curveTypeString = EllipticCurves.curveToString(curveTypeSubtle);
         expect(jwk?.['kty']).toBe('EC');
         expect(jwk?.['crv']).toBe(curveTypeString);
         expect(
             Bytes.fromBase64(assertExists(jwk['x']), /* opt_webSafe = */ true))
             .toEqual(x);
         expect(
             Bytes.fromBase64(assertExists(jwk['y']), /* opt_webSafe = */ true))
             .toEqual(y);
         expect(jwk?.['d']).toEqual(undefined);
         expect(jwk?.['ext']).toBe(true);
       }
     });

  it('get json web key from proto, public key, leading nonzero',
     async function() {
       const curve = PbEllipticCurveType.NIST_P256;
       const key = await createKey(curve);
       const publicKey = assertExists(key.getPublicKey());
       const x = bytesAsU8(publicKey.getX());
       publicKey.setX(Bytes.concat(new Uint8Array([1, 0]), x));
       try {
         EciesAeadHkdfUtil.getJsonWebKeyFromProto(publicKey);
         fail('An exception should be thrown.');
       } catch (e: any) {
         expect(e.toString())
             .toBe(
                 'SecurityException: Number needs more bytes to be represented.');
       }
     });

  it('get json web key from proto, private key', async function() {
    for (const curve
             of [PbEllipticCurveType.NIST_P256,
                 PbEllipticCurveType.NIST_P384,
                 PbEllipticCurveType.NIST_P521,
    ]) {
      const key = await createKey(curve);
      const jwk = EciesAeadHkdfUtil.getJsonWebKeyFromProto(key);

      // Test the returned jwk.
      const curveTypeSubtle = Util.curveTypeProtoToSubtle(curve);
      const curveTypeString = EllipticCurves.curveToString(curveTypeSubtle);
      const publicKey = assertExists(key.getPublicKey());
      expect(jwk?.['kty']).toBe('EC');
      expect(jwk?.['crv']).toBe(curveTypeString);
      expect(Bytes.fromBase64(assertExists(jwk['x']), /* opt_webSafe = */ true))
          .toEqual(bytesAsU8(publicKey.getX()));
      expect(Bytes.fromBase64(assertExists(jwk['y']), /* opt_webSafe = */ true))
          .toEqual(bytesAsU8(publicKey.getY()));
      expect(Bytes.fromBase64(assertExists(jwk['d']), /* opt_webSafe = */ true))
          .toEqual(bytesAsU8(key.getKeyValue()));
      expect(jwk?.['ext']).toBe(true);
    }
  });

  it('get json web key from proto, private key, leading zeros',
     async function() {
       for (const curve
                of [PbEllipticCurveType.NIST_P256,
                    PbEllipticCurveType.NIST_P384,
                    PbEllipticCurveType.NIST_P521,
       ]) {
         const key = await createKey(curve);
         const d = bytesAsU8(key.getKeyValue());
         key.setKeyValue(Bytes.concat(new Uint8Array([0, 0, 0]), d));
         const jwk = EciesAeadHkdfUtil.getJsonWebKeyFromProto(key);

         // Test the returned jwk.
         const curveTypeSubtle = Util.curveTypeProtoToSubtle(curve);
         const curveTypeString = EllipticCurves.curveToString(curveTypeSubtle);

         const publicKey = assertExists(key.getPublicKey());
         expect(jwk?.['kty']).toBe('EC');
         expect(jwk?.['crv']).toBe(curveTypeString);
         expect(
             Bytes.fromBase64(assertExists(jwk['x']), /* opt_webSafe = */ true))
             .toEqual(bytesAsU8(publicKey.getX()));
         expect(
             Bytes.fromBase64(assertExists(jwk['y']), /* opt_webSafe = */ true))
             .toEqual(bytesAsU8(publicKey.getY()));
         expect(
             Bytes.fromBase64(assertExists(jwk['d']), /* opt_webSafe = */ true))
             .toEqual(d);
         expect(jwk['ext']).toBe(true);
       }
     });
});

function createKemParams(
    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
    opt_hashType: PbHashType = PbHashType.SHA256): PbEciesHkdfKemParams {
  const kemParams = new PbEciesHkdfKemParams()
                        .setCurveType(opt_curveType)
                        .setHkdfHashType(opt_hashType);

  return kemParams;
}

function createDemParams(opt_keyTemplate?: PbKeyTemplate):
    PbEciesAeadDemParams {
  if (!opt_keyTemplate) {
    opt_keyTemplate = AeadKeyTemplates.aes128CtrHmacSha256();
  }

  const demParams = new PbEciesAeadDemParams().setAeadDem(opt_keyTemplate);

  return demParams;
}

function createKeyParams(
    opt_curveType?: PbEllipticCurveType, opt_hashType?: PbHashType,
    opt_keyTemplate?: PbKeyTemplate,
    opt_pointFormat: PbPointFormat =
        PbPointFormat.UNCOMPRESSED): PbEciesAeadHkdfParams {
  const params = new PbEciesAeadHkdfParams()
                     .setKemParams(createKemParams(opt_curveType, opt_hashType))
                     .setDemParams(createDemParams(opt_keyTemplate))
                     .setEcPointFormat(opt_pointFormat);

  return params;
}

async function createKey(
    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
    opt_hashType?: PbHashType, opt_keyTemplate?: PbKeyTemplate,
    opt_pointFormat?: PbPointFormat): Promise<PbEciesAeadHkdfPrivateKey> {
  const curveTypeSubtle = Util.curveTypeProtoToSubtle((opt_curveType));
  const curveName = EllipticCurves.curveToString(curveTypeSubtle);

  const publicKeyProto =
      new PbEciesAeadHkdfPublicKey().setVersion(0).setParams(createKeyParams(
          opt_curveType, opt_hashType, opt_keyTemplate, opt_pointFormat));


  const keyPair = await EllipticCurves.generateKeyPair('ECDH', curveName);
  const publicKeyJson =
      await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
  publicKeyProto.setX(Bytes.fromBase64(
      assertExists(publicKeyJson['x']), /* opt_webSafe = */ true));
  publicKeyProto.setY(Bytes.fromBase64(
      assertExists(publicKeyJson['y']), /* opt_webSafe = */ true));

  const privateKeyProto = new PbEciesAeadHkdfPrivateKey();
  const privateKeyJson =
      await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
  privateKeyProto.setKeyValue(Bytes.fromBase64(
      assertExists(privateKeyJson['d']), /* opt_webSafe = */ true));
  privateKeyProto.setVersion(0);
  privateKeyProto.setPublicKey(publicKeyProto);

  return privateKeyProto;
}
