/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import * as EllipticCurves from '../subtle/elliptic_curves';

import {PbEllipticCurveType, PbHashType, PbKeyData, PbKeyset, PbKeysetKey, PbKeyStatusType, PbOutputPrefixType, PbPointFormat} from './proto';
import * as Util from './util';

////////////////////////////////////////////////////////////////////////////////
// tests
////////////////////////////////////////////////////////////////////////////////

describe('util test', function() {
  // tests for validateKey method
  it('validate key missing key data', async function() {
    const key = createKey().setKeyData(null);

    try {
      Util.validateKey(key);
    } catch (e: any) {
      expect(e.toString())
          .toBe(ExceptionText.InvalidKeyMissingKeyData(key.getKeyId()));
      return;
    }
    fail('An exception should be thrown.');
  });

  it('validate key unknown prefix', async function() {
    const key =
        createKey().setOutputPrefixType(PbOutputPrefixType.UNKNOWN_PREFIX);

    try {
      Util.validateKey(key);
    } catch (e: any) {
      expect(e.toString())
          .toBe(ExceptionText.InvalidKeyUnknownPrefix(key.getKeyId()));
      return;
    }
    fail('An exception should be thrown.');
  });

  it('validate key unknown status', async function() {
    const key = createKey().setStatus(PbKeyStatusType.UNKNOWN_STATUS);

    try {
      Util.validateKey(key);
    } catch (e: any) {
      expect(e.toString())
          .toBe(ExceptionText.InvalidKeyUnknownStatus(key.getKeyId()));
      return;
    }
    fail('An exception should be thrown.');
  });

  it('validate key valid keys', async function() {
    Util.validateKey(createKey());
    Util.validateKey(
        createKey(/* opt_keyId = */ 0xAABBCCDD, /* opt_enabled = */ true));
    Util.validateKey(
        createKey(/* opt_keyId = */ 0xABCDABCD, /* opt_enabled = */ false));
  });

  // tests for validateKeyset method
  it('validate keyset without keys', async function() {
    const keyset = new PbKeyset();

    try {
      Util.validateKeyset(keyset);
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.InvalidKeysetMissingKeys());
      return;
    }
    fail('An exception should be thrown.');
  });

  it('validate keyset disabled primary', async function() {
    const keyset = createKeyset();
    keyset.addKey(
        createKey(/* opt_id = */ 0xFFFFFFFF, /* opt_enabled = */ false));
    keyset.setPrimaryKeyId(0xFFFFFFFF);

    try {
      Util.validateKeyset(keyset);
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.InvalidKeysetDisabledPrimary());
      return;
    }
    fail('An exception should be thrown.');
  });

  it('validate keyset multiple primaries', async function() {
    const keyset = createKeyset();
    const key =
        createKey(/* opt_id = */ 0xFFFFFFFF, /* opt_enabled = */ true);
    keyset.addKey(key);
    keyset.addKey(key);
    keyset.setPrimaryKeyId(0xFFFFFFFF);

    try {
      Util.validateKeyset(keyset);
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.InvalidKeysetMultiplePrimaries());
      return;
    }
    fail('An exception should be thrown.');
  });

  it('validate keyset with invalid key', async function() {
    const keyset = createKeyset();
    const key =
        createKey(4294967295, true).setStatus(PbKeyStatusType.UNKNOWN_STATUS);
    keyset.addKey(key);

    try {
      Util.validateKeyset(keyset);
    } catch (e: any) {
      expect(e.toString())
          .toBe(ExceptionText.InvalidKeyUnknownStatus(key.getKeyId()));
      return;
    }
    fail('An exception should be thrown.');
  });

  it('validate keyset with valid keyset', async function() {
    const keyset = createKeyset();

    Util.validateKeyset(keyset);
  });

  // tests for protoToSubtle methods
  it('curve type proto to subtle', function() {
    expect(Util.curveTypeProtoToSubtle(PbEllipticCurveType.NIST_P256))
        .toBe(EllipticCurves.CurveType.P256);
    expect(Util.curveTypeProtoToSubtle(PbEllipticCurveType.NIST_P384))
        .toBe(EllipticCurves.CurveType.P384);
    expect(Util.curveTypeProtoToSubtle(PbEllipticCurveType.NIST_P521))
        .toBe(EllipticCurves.CurveType.P521);
  });

  it('point format proto to subtle', function() {
    expect(Util.pointFormatProtoToSubtle(PbPointFormat.UNCOMPRESSED))
        .toBe(EllipticCurves.PointFormatType.UNCOMPRESSED);
    expect(Util.pointFormatProtoToSubtle(PbPointFormat.COMPRESSED))
        .toBe(EllipticCurves.PointFormatType.COMPRESSED);
    expect(Util.pointFormatProtoToSubtle(
               PbPointFormat.DO_NOT_USE_CRUNCHY_UNCOMPRESSED))
        .toBe(EllipticCurves.PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED);
  });

  it('hash type proto to string', function() {
    expect(Util.hashTypeProtoToString(PbHashType.SHA1)).toBe('SHA-1');
    expect(Util.hashTypeProtoToString(PbHashType.SHA256)).toBe('SHA-256');
    expect(Util.hashTypeProtoToString(PbHashType.SHA512)).toBe('SHA-512');
  });
});


/**
 * Class which holds texts for each type of exception.
 * @final
 */
class ExceptionText {
  // Exceptions for invalid keys.
  static InvalidKeyMissingKeyData(keyId: number): string {
    return 'SecurityException: Key data are missing for key ' + keyId + '.';
  }
  static InvalidKeyUnknownPrefix(keyId: number): string {
    return 'SecurityException: Key ' + keyId +
        ' has unknown output prefix type.';
  }
  static InvalidKeyUnknownStatus(keyId: number): string {
    return 'SecurityException: Key ' + keyId + ' has unknown status.';
  }

  // Exceptions for invalid keysets.
  static InvalidKeysetMissingKeys(): string {
    return 'SecurityException: Keyset should be non null and ' +
        'must contain at least one key.';
  }
  static InvalidKeysetDisabledPrimary(): string {
    return 'SecurityException: Primary key has to be in the keyset and ' +
        'has to be enabled.';
  }
  static InvalidKeysetMultiplePrimaries(): string {
    return 'SecurityException: Primary key has to be unique.';
  }
}

/** Returns a valid PbKeysetKey. */
function createKey(
    opt_id: number = 0x12345678, opt_enabled: boolean = true,
    opt_publicKey: boolean = false): PbKeysetKey {
  const keyData =
      new PbKeyData().setTypeUrl('someTypeUrl').setValue(new Uint8Array(10));
  if (opt_publicKey) {
    keyData.setKeyMaterialType(PbKeyData.KeyMaterialType.ASYMMETRIC_PUBLIC);
  } else {
    keyData.setKeyMaterialType(PbKeyData.KeyMaterialType.SYMMETRIC);
  }

  const key = new PbKeysetKey().setKeyData(keyData);
  if (opt_enabled) {
    key.setStatus(PbKeyStatusType.ENABLED);
  } else {
    key.setStatus(PbKeyStatusType.DISABLED);
  }
  key.setKeyId(opt_id);
  key.setOutputPrefixType(PbOutputPrefixType.TINK);

  return key;
}

/** Returns a valid PbKeyset which primary has id equal to 1. */
function createKeyset(): PbKeyset {
  const numberOfKeys = 20;

  const keyset = new PbKeyset();
  for (let i = 0; i < numberOfKeys; i++) {
    // Key id is never set to 0 as primaryKeyId = 0 if it is unset.
    const key = createKey(i + 1, /* opt_enabled = */ (i % 2) < 1, (i % 4) < 2);
    keyset.addKey(key);
  }

  keyset.setPrimaryKeyId(1);
  return keyset;
}
