/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {PbAesCtrKey, PbAesCtrKeyFormat, PbAesGcmKey, PbAesGcmKeyFormat, PbKeyData} from '../internal/proto';
import {bytesLength} from '../internal/proto_shims';
import * as Random from '../subtle/random';

import {AesGcmKeyManager} from './aes_gcm_key_manager';
import {Aead} from './internal/aead';

const KEY_TYPE = 'type.googleapis.com/google.crypto.tink.AesGcmKey';
const VERSION = 0;
const PRIMITIVE = Aead;

describe('aes gcm key manager test', function() {
  /////////////////////////////////////////////////////////////////////////////
  // tests for newKey method

  // newKey method -- key formats
  it('new key, invalid key format', function() {
    const keyFormat = new PbAesCtrKeyFormat();
    const manager = new AesGcmKeyManager();

    try {
      manager.getKeyFactory().newKey(keyFormat);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.invalidKeyFormat());
    }
  });

  it('new key, invalid serialized key format', function() {
    const keyFormat = new Uint8Array(0);
    const manager = new AesGcmKeyManager();

    try {
      manager.getKeyFactory().newKey(keyFormat);
      fail('An exception should be thrown.');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.invalidSerializedKeyFormat());
    }
  });

  it('new key, unsupported key sizes', function() {
    const manager = new AesGcmKeyManager();

    for (let keySize = 0; keySize < 40; keySize++) {
      if (keySize === 16 || keySize === 32) {
        // Keys of size 16 and 32 bytes are supported.
        continue;
      }
      const keyFormat = createTestKeyFormat(keySize);

      try {
        manager.getKeyFactory().newKey(keyFormat);
        fail('An exception should be thrown.');
      } catch (e: any) {
        expect(e.toString()).toBe(ExceptionText.unsupportedKeySize(keySize));
      }
    }
  });

  it('new key, via format proto', function() {
    const manager = new AesGcmKeyManager();

    const keyFormat = createTestKeyFormat();

    const key = manager.getKeyFactory().newKey(keyFormat);

    expect(bytesLength(key.getKeyValue())).toBe(keyFormat.getKeySize());
  });

  it('new key, via serialized format proto', function() {
    const manager = new AesGcmKeyManager();

    const keyFormat = createTestKeyFormat();
    const serializedKeyFormat = keyFormat.serializeBinary();

    const key = manager.getKeyFactory().newKey(serializedKeyFormat);

    expect(bytesLength(key.getKeyValue())).toBe(keyFormat.getKeySize());
  });

  /////////////////////////////////////////////////////////////////////////////
  // tests for NewKeyData method

  it('new key data, should work', function() {
    const keyFormat = createTestKeyFormat();
    const serializedKeyFormat = keyFormat.serializeBinary();
    const manager = new AesGcmKeyManager();

    const keyData = manager.getKeyFactory().newKeyData(serializedKeyFormat);

    expect(keyData.getTypeUrl()).toBe(KEY_TYPE);
    expect(keyData.getKeyMaterialType())
        .toBe(PbKeyData.KeyMaterialType.SYMMETRIC);

    const key = PbAesGcmKey.deserializeBinary(keyData.getValue());

    expect(bytesLength(key.getKeyValue())).toBe(keyFormat.getKeySize());
  });

  /////////////////////////////////////////////////////////////////////////////
  // tests for getPrimitive method

  it('get primitive, unsupported key data type', async function() {
    const manager = new AesGcmKeyManager();
    const keyData = createTestKeyData().setTypeUrl('bad_type_url');

    try {
      await manager.getPrimitive(PRIMITIVE, keyData);
      fail('An exception should be thrown');
    } catch (e: any) {
      expect(e.toString())
          .toBe(ExceptionText.unsupportedKeyType(keyData.getTypeUrl()));
    }
  });

  it('get primitive, unsupported key type', async function() {
    const manager = new AesGcmKeyManager();
    const key = new PbAesCtrKey();

    try {
      await manager.getPrimitive(PRIMITIVE, key);
      fail('An exception should be thrown');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.unsupportedKeyType());
    }
  });

  it('get primitive, bad version', async function() {
    const version = 1;
    const manager = new AesGcmKeyManager();
    const key = createTestKey().setVersion(version);

    try {
      await manager.getPrimitive(PRIMITIVE, key);
      fail('An exception should be thrown');
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.versionOutOfBounds());
    }
  });

  it('get primitive, unsupported key sizes', async function() {
    const manager = new AesGcmKeyManager();

    for (let keySize = 0; keySize < 40; keySize++) {
      if (keySize === 16 || keySize === 32) {
        // Keys of sizes 16 and 32 bytes are supported.
        continue;
      }

      const key: PbAesGcmKey = createTestKey(keySize);
      try {
        await manager.getPrimitive(PRIMITIVE, key);
        fail('An exception should be thrown');
      } catch (e: any) {
        expect(e.toString()).toBe(ExceptionText.unsupportedKeySize(keySize));
      }
    }
  });

  it('get primitive, bad serialization', async function() {
    const manager = new AesGcmKeyManager();
    const keyData = createTestKeyData().setValue(new Uint8Array([0]));

    try {
      await manager.getPrimitive(PRIMITIVE, keyData);
      fail('An exception should be thrown');
    } catch (e: any) {
      let message = e.toString();
      if (message === ExceptionText.unsupportedKeySize(0)) {
        message = ExceptionText.invalidSerializedKey();
      }
      expect(message).toBe(ExceptionText.invalidSerializedKey());
    }
  });

  // Tests for getting primitive from valid key/keyData.
  it('get primitive, from key', async function() {
    const manager = new AesGcmKeyManager();
    const key = createTestKey();

    // Get the primitive from key manager.
    const primitive: Aead = await manager.getPrimitive(PRIMITIVE, key);

    // Test the returned primitive.
    const plaintext = Random.randBytes(8);
    const aad = Random.randBytes(8);
    const ciphertext = await primitive.encrypt(plaintext, aad);
    const decryptedCiphertext = await primitive.decrypt(ciphertext, aad);

    expect(decryptedCiphertext).toEqual(plaintext);
  });

  it('get primitive, from key data', async function() {
    const manager = new AesGcmKeyManager();
    const keyData = createTestKeyData();

    // Get primitive.
    const primitive: Aead = await manager.getPrimitive(PRIMITIVE, keyData);

    // Test the returned primitive.
    const plaintext = Random.randBytes(8);
    const aad = Random.randBytes(8);
    const ciphertext = await primitive.encrypt(plaintext, aad);
    const decryptedCiphertext = await primitive.decrypt(ciphertext, aad);

    expect(decryptedCiphertext).toEqual(plaintext);
  });

  /////////////////////////////////////////////////////////////////////////////
  // tests for getVersion, getKeyType and doesSupport methods

  it('get version, should be zero', function() {
    const manager = new AesGcmKeyManager();
    expect(manager.getVersion()).toBe(0);
  });

  it('get key type, should be aes gcm key type', function() {
    const manager = new AesGcmKeyManager();
    expect(manager.getKeyType()).toBe(KEY_TYPE);
  });

  it('does support, should support aes gcm key type', function() {
    const manager = new AesGcmKeyManager();
    expect(manager.doesSupport(KEY_TYPE)).toBe(true);
  });

  it('get primitive type, should be aead', function() {
    const manager = new AesGcmKeyManager();
    expect(manager.getPrimitiveType()).toBe(PRIMITIVE);
  });
});

/////////////////////////////////////////////////////////////////////////////
// Helper functions for tests

class ExceptionText {
  static unsupportedPrimitive(): string {
    return 'SecurityException: Requested primitive type which is not supported ' +
        'by this key manager.';
  }

  static unsupportedKeySize(keySize: number): string {
    return 'InvalidArgumentsException: unsupported AES key size: ' + keySize;
  }

  static versionOutOfBounds(): string {
    return 'SecurityException: Version is out of bound, must be between 0 and ' +
        VERSION + '.';
  }

  static unsupportedKeyType(opt_unsupportedKeyType?: string): string {
    const prefix = 'SecurityException: Key type';
    const suffix =
        'is not supported. This key manager supports ' + KEY_TYPE + '.';

    if (opt_unsupportedKeyType) {
      return prefix + ' ' + opt_unsupportedKeyType + ' ' + suffix;
    } else {
      return prefix + ' ' + suffix;
    }
  }

  static invalidSerializedKey(): string {
    return 'SecurityException: Could not parse the input as a serialized proto of ' +
        KEY_TYPE + ' key.';
  }

  static invalidSerializedKeyFormat() {
    return 'SecurityException: Could not parse the input as a serialized proto of ' +
        KEY_TYPE + ' key format.';
  }

  static invalidKeyFormat(): string {
    return 'SecurityException: Expected AesGcmKeyFormat-proto';
  }
}

function createTestKeyFormat(opt_keySize: number = 16): PbAesGcmKeyFormat {
  const keyFormat = new PbAesGcmKeyFormat().setKeySize(opt_keySize);
  return keyFormat;
}

function createTestKey(opt_keySize: number = 16): PbAesGcmKey {
  const key = new PbAesGcmKey().setVersion(0).setKeyValue(
      Random.randBytes(opt_keySize));

  return key;
}

function createTestKeyData(opt_keySize?: number): PbKeyData {
  const keyData = new PbKeyData()
                      .setTypeUrl(KEY_TYPE)
                      .setValue(createTestKey(opt_keySize).serializeBinary())
                      .setKeyMaterialType(PbKeyData.KeyMaterialType.SYMMETRIC);

  return keyData;
}
