/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {AeadKeyTemplates} from '../aead/aead_key_templates';
import {PbEciesAeadDemParams, PbEciesAeadHkdfKeyFormat, PbEciesAeadHkdfParams, PbEciesAeadHkdfPrivateKey, PbEciesAeadHkdfPublicKey, PbEciesHkdfKemParams, PbEllipticCurveType, PbHashType, PbKeyTemplate, PbPointFormat} from '../internal/proto';
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 EciesAeadHkdfValidators from './ecies_aead_hkdf_validators';


describe('ecies aead hkdf validators 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('validate params, missing kem params', function() {
    const invalidParams = createParams().setKemParams(null);

    try {
      EciesAeadHkdfValidators.validateParams(invalidParams);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingKemParams());
    }
  });

  it('validate params, invalid kem params, unknown hash type', function() {
    const invalidParams = createParams();
    invalidParams.getKemParams()?.setHkdfHashType(PbHashType.UNKNOWN_HASH);

    try {
      EciesAeadHkdfValidators.validateParams(invalidParams);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.unknownHashType());
    }
  });

  it('validate params, invalid kem params, unknown curve type', function() {
    const invalidParams = createParams();
    invalidParams.getKemParams()?.setCurveType(
        PbEllipticCurveType.UNKNOWN_CURVE);

    try {
      EciesAeadHkdfValidators.validateParams(invalidParams);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.unknownCurveType());
    }
  });

  it('validate params, missing dem params', function() {
    const invalidParams = createParams().setDemParams(null);

    try {
      EciesAeadHkdfValidators.validateParams(invalidParams);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingDemParams());
    }
  });

  it('validate params, invalid dem params, missing aead template', function() {
    const invalidParams = createParams();
    invalidParams.getDemParams()?.setAeadDem(null);

    try {
      EciesAeadHkdfValidators.validateParams(invalidParams);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingAeadTemplate());
    }
  });

  it('validate params, invalid dem params, unsupported aead template',
     function() {
       const unsupportedTypeUrl = 'UNSUPPORTED_KEY_TYPE_URL';
       const invalidParams = createParams();
       invalidParams.getDemParams()?.getAeadDem()?.setTypeUrl(
           unsupportedTypeUrl);

       try {
         EciesAeadHkdfValidators.validateParams(invalidParams);
         fail('An exception should be thrown.');
       } catch (e: any) {
         expect(e.toString())
             .toBe(ExceptionText.unsupportedKeyTemplate(unsupportedTypeUrl));
       }
     });

  it('validate params, unknown point format', function() {
    const invalidParams =
        createParams().setEcPointFormat(PbPointFormat.UNKNOWN_FORMAT);

    try {
      EciesAeadHkdfValidators.validateParams(invalidParams);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.unknownPointFormat());
    }
  });
  it('validate params, different valid values', function() {
    for (const curve
             of [PbEllipticCurveType.NIST_P256,
                 PbEllipticCurveType.NIST_P384,
                 PbEllipticCurveType.NIST_P521,
    ]) {
      for (const hashType
               of [PbHashType.SHA1,
                   PbHashType.SHA384,
                   PbHashType.SHA256,
                   PbHashType.SHA512,
      ]) {
        for (const keyTemplate
                 of [AeadKeyTemplates.aes128CtrHmacSha256(),
                     AeadKeyTemplates.aes128Gcm(),
        ]) {
          for (const pointFormat
                   of [PbPointFormat.UNCOMPRESSED,
                       PbPointFormat.COMPRESSED,
                       PbPointFormat.DO_NOT_USE_CRUNCHY_UNCOMPRESSED,
          ]) {
            const params =
                createParams(curve, hashType, keyTemplate, pointFormat);
            EciesAeadHkdfValidators.validateParams(params);
          }
        }
      }
    }
  });

  it('validate key format, missing params', function() {
    const invalidKeyFormat = new PbEciesAeadHkdfKeyFormat();

    try {
      EciesAeadHkdfValidators.validateKeyFormat(invalidKeyFormat);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingFormatParams());
    }
  });

  it('validate key format, invalid params', function() {
    const invalidKeyFormat =
        new PbEciesAeadHkdfKeyFormat().setParams(createParams());

    // Check that also params were checked.
    // Test missing DEM params.
    invalidKeyFormat.getParams()?.setDemParams(null);
    try {
      EciesAeadHkdfValidators.validateKeyFormat(invalidKeyFormat);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingDemParams());
    }
    invalidKeyFormat.getParams()?.setDemParams(createDemParams());

    // Test UNKNOWN_HASH in KEM params.
    invalidKeyFormat.getParams()?.getKemParams()?.setHkdfHashType(
        PbHashType.UNKNOWN_HASH);
    try {
      EciesAeadHkdfValidators.validateKeyFormat(invalidKeyFormat);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.unknownHashType());
    }
  });

  it('validate public key, missing params', function() {
    const invalidPublicKey = new PbEciesAeadHkdfPublicKey();

    try {
      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingKeyParams());
    }
  });

  it('validate public key, missing values x y', function() {
    const invalidPublicKey =
        new PbEciesAeadHkdfPublicKey().setParams(createParams());

    // Both X and Y are set to empty.
    try {
      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingXY());
    }

    // The key with only Y set to empty is also invalid.
    invalidPublicKey.setX(new Uint8Array(10));
    try {
      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingXY());
    }

    // The key with only X set to empty is also invalid.
    invalidPublicKey.setY(new Uint8Array(10));
    invalidPublicKey.setX(new Uint8Array(0));
    try {
      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingXY());
    }
  });

  it('validate public key, invalid params', async function() {
    const invalidPublicKey = await createPublicKey();

    // Check that also params were checked.
    // Test missing DEM params.
    invalidPublicKey.getParams()?.setDemParams(null);
    try {
      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingDemParams());
    }
    invalidPublicKey.getParams()?.setDemParams(createDemParams());

    // Test UNKNOWN_HASH in KEM params.
    invalidPublicKey.getParams()?.getKemParams()?.setHkdfHashType(
        PbHashType.UNKNOWN_HASH);
    try {
      EciesAeadHkdfValidators.validatePublicKey(invalidPublicKey, 0);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.unknownHashType());
    }
  });

  it('validate public key, version out of bounds', async function() {
    const managerVersion = 0;
    const invalidPublicKey = (await createPublicKey()).setVersion(1);
    try {
      EciesAeadHkdfValidators.validatePublicKey(
          invalidPublicKey, managerVersion);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString())
          .toBe(ExceptionText.versionOutOfBounds(managerVersion));
    }
  });

  it('validate private key, missing public key', async function() {
    const invalidPrivateKey = (await createPrivateKey()).setPublicKey(null);
    try {
      EciesAeadHkdfValidators.validatePrivateKey(invalidPrivateKey, 0, 0);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingPublicKey());
    }
  });

  it('validate private key, invalid public key', async function() {
    const invalidPrivateKey = await createPrivateKey();
    invalidPrivateKey.getPublicKey()?.setParams(null);
    try {
      EciesAeadHkdfValidators.validatePrivateKey(invalidPrivateKey, 0, 0);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.missingKeyParams());
    }
  });

  it('validate private key, should work', async function() {
    const privateKey = await createPrivateKey();
    EciesAeadHkdfValidators.validatePrivateKey(privateKey, 0, 0);
  });

  it('validate private key, version out of bounds', async function() {
    const managerVersion = 0;
    const invalidPrivateKey = (await createPrivateKey()).setVersion(1);
    try {
      EciesAeadHkdfValidators.validatePrivateKey(
          invalidPrivateKey, managerVersion, managerVersion);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString())
          .toBe(ExceptionText.versionOutOfBounds(managerVersion));
    }
  });
});

// Helper classes and functions
class ExceptionText {
  static missingFormatParams(): string {
    return 'SecurityException: Invalid key format - missing key params.';
  }

  static missingKeyParams(): string {
    return 'SecurityException: Invalid public key - missing key params.';
  }

  static unknownPointFormat(): string {
    return 'SecurityException: Invalid key params - unknown EC point format.';
  }

  static missingKemParams(): string {
    return 'SecurityException: Invalid params - missing KEM params.';
  }

  static unknownHashType(): string {
    return 'SecurityException: Invalid KEM params - unknown hash type.';
  }

  static unknownCurveType(): string {
    return 'SecurityException: Invalid KEM params - unknown curve type.';
  }

  static missingDemParams(): string {
    return 'SecurityException: Invalid params - missing DEM params.';
  }

  static missingAeadTemplate(): string {
    return 'SecurityException: Invalid DEM params - missing AEAD key template.';
  }

  static unsupportedKeyTemplate(templateTypeUrl: string): string {
    return 'SecurityException: Invalid DEM params - ' + templateTypeUrl +
        ' template is not supported by ECIES AEAD HKDF.';
  }

  static missingXY(): string {
    return 'SecurityException: Invalid public key - missing value of X or Y.';
  }

  static missingPublicKey(): string {
    return 'SecurityException: Invalid private key - missing public key information.';
  }

  static missingPrivateKeyValue(): string {
    return 'SecurityException: Invalid private key - missing private key value.';
  }

  static versionOutOfBounds(version: number): string {
    return 'SecurityException: Version is out of bound, must be between 0 and ' +
        version + '.';
  }
}

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 createParams(
    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 createPrivateKey(
    opt_curveType: PbEllipticCurveType = PbEllipticCurveType.NIST_P256,
    opt_hashType?: PbHashType, opt_keyTemplate?: PbKeyTemplate,
    opt_pointFormat?: PbPointFormat): Promise<PbEciesAeadHkdfPrivateKey> {
  const curveSubtleType = Util.curveTypeProtoToSubtle(opt_curveType);
  const curveName = EllipticCurves.curveToString(curveSubtleType);

  const publicKeyProto =
      new PbEciesAeadHkdfPublicKey().setVersion(0).setParams(createParams(
          opt_curveType, opt_hashType, opt_keyTemplate, opt_pointFormat));
  const keyPair = await EllipticCurves.generateKeyPair('ECDH', curveName);
  const jsonPublicKey =
      await EllipticCurves.exportCryptoKey(keyPair.publicKey!);
  publicKeyProto.setX(Bytes.fromBase64(
      assertExists(jsonPublicKey['x']), /* opt_webSafe = */ true));
  publicKeyProto.setY(Bytes.fromBase64(
      assertExists(jsonPublicKey['y']), /* opt_webSafe = */ true));


  const privateKeyProto =
      new PbEciesAeadHkdfPrivateKey().setVersion(0).setPublicKey(
          publicKeyProto);
  const jsonPrivateKey =
      await EllipticCurves.exportCryptoKey(keyPair.privateKey!);
  privateKeyProto.setKeyValue(Bytes.fromBase64(
      assertExists(jsonPrivateKey['d']), /* opt_webSafe = */ true));

  return privateKeyProto;
}

async function createPublicKey(
    opt_curveType?: PbEllipticCurveType, opt_hashType?: PbHashType,
    opt_keyTemplate?: PbKeyTemplate,
    opt_pointFormat?: PbPointFormat): Promise<PbEciesAeadHkdfPublicKey> {
  const key = await createPrivateKey(
      opt_curveType, opt_hashType, opt_keyTemplate, opt_pointFormat);
  return assertExists(key.getPublicKey());
}
