/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {Aead} from '../aead';
import {SecurityException} from '../exception/security_exception';

import {CryptoFormat} from './crypto_format';
import * as PrimitiveSet from './primitive_set';
import {PbKeysetKey, PbKeyStatusType, PbOutputPrefixType} from './proto';

describe('primitive set test', function() {
  /////////////////////////////////////////////////////////////////////////////
  // tests for addPrimitive method
  it('add primitive unknown crypto format', function() {
    const primitive = new DummyAead1();
    const key =
        createKey().setOutputPrefixType(PbOutputPrefixType.UNKNOWN_PREFIX);
    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);

    try {
      primitiveSet.addPrimitive(primitive, key);
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.unknownPrefixType());
      return;
    }
    fail('An exception should be thrown.');
  });
  it('add primitive multiple times should work', function() {
    const key = createKey();
    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);

    for (let i = 0; i < 4; i++) {
      let primitive;
      if (i % 2 === 0) {
        primitive = new DummyAead1();
      } else {
        primitive = new DummyAead2();
      }
      const result = primitiveSet.addPrimitive(primitive, key);

      expect(result.getPrimitive()).toEqual(primitive);
      expect(result.getKeyStatus()).toBe(key.getStatus());
      expect(result.getOutputPrefixType()).toBe(key.getOutputPrefixType());
      expect(result.getIdentifier()).toEqual(CryptoFormat.getOutputPrefix(key));
    }
  });

  /////////////////////////////////////////////////////////////////////////////
  // tests for getPrimitives method
  it('get primitives which were not added', function() {
    // Fill in the structure with some primitives.
    const numberOfAddedPrimitives = 12;
    const primitiveSet = initPrimitiveSet(numberOfAddedPrimitives);

    const key = createKey(/* opt_keyId = */ numberOfAddedPrimitives + 1);
    const identifier = CryptoFormat.getOutputPrefix(key);
    const result = primitiveSet.getPrimitives(identifier);

    expect(result).toEqual([]);
  });

  it('get primitives different identifiers', function() {
    // Fill in the structure with some primitives.
    const n = 100;
    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);

    const added = [];
    for (let id = 0; id < n; id++) {
      const legacyKeyType = ((id % 2) < 1);
      const enabledKey = ((id % 4) < 2);
      const key = createKey(id, legacyKeyType, enabledKey);

      let primitive;
      if  ((id % 8) < 4) {
        primitive = new DummyAead1();
      } else {
        primitive = new DummyAead2();
      }

      const res = primitiveSet.addPrimitive(primitive, key);
      added.push({key, entry: res});
    }

    // Test that getPrimitives return correct value for them.
    const addedLength = added.length;
    for (let i = 0; i < addedLength; i++) {
      const identifier = CryptoFormat.getOutputPrefix(added[i].key);
      // Should return a set containing only one primitive as each added
      // primitive has different identifier.
      const expectedResult = [added[i].entry];
      const result = primitiveSet.getPrimitives(identifier);

      expect(result).toEqual(expectedResult);
    }
  });

  it('get primitives same identifiers', function() {
    // Fill in the structure with some primitives.
    const numberOfAddedPrimitives = 50;
    const primitiveSet = initPrimitiveSet(numberOfAddedPrimitives);

    // Add a group of primitives with same identifier.
    const n = 12;
    const keyId = 0xABCDEF98;
    const legacyKeyType = false;

    const expectedResult = [];
    for (let i = 0; i < n; i++) {
      const enabledKey = ((i % 2) < 1);
      const key = createKey(keyId, legacyKeyType, enabledKey);

      let primitive;
      if  ((i % 4) < 2) {
        primitive = new DummyAead1();
      } else {
        primitive = new DummyAead2();
      }

      const res = primitiveSet.addPrimitive(primitive, key);
      expectedResult.push(res);
    }

    const identifier =
        CryptoFormat.getOutputPrefix(createKey(keyId, legacyKeyType));
    const result = primitiveSet.getPrimitives(identifier);

    expect(result).toEqual(expectedResult);
  });

  /////////////////////////////////////////////////////////////////////////////
  // tests for getRawPrimitives method
  it('get raw primitives', function() {
    const numberOfAddedPrimitives = 20;
    const primitiveSet = initPrimitiveSet(numberOfAddedPrimitives);

    // No RAW primitives were added.
    const expectedResult: Array<PrimitiveSet.Entry<Aead>> = [];
    let result = primitiveSet.getRawPrimitives();
    expect(result).toEqual(expectedResult);

    // Add RAW primitives and check the result again after each adding.
    const key = createKey().setOutputPrefixType(PbOutputPrefixType.RAW);

    let addResult = primitiveSet.addPrimitive(new DummyAead1(), key);
    expectedResult.push(addResult);
    result = primitiveSet.getRawPrimitives();
    expect(result).toEqual(expectedResult);

    key.setStatus(PbKeyStatusType.DISABLED);
    addResult = primitiveSet.addPrimitive(new DummyAead2(), key);
    expectedResult.push(addResult);
    result = primitiveSet.getRawPrimitives();
    expect(result).toEqual(expectedResult);
  });

  /////////////////////////////////////////////////////////////////////////////
  // tests for setPrimary and getPrimary methods
  it('set primary to nonholded entry', function() {
    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
    const entry = new PrimitiveSet.Entry(
        new DummyAead1(), new Uint8Array(10), PbKeyStatusType.ENABLED,
        PbOutputPrefixType.TINK);

    try {
      primitiveSet.setPrimary(entry);
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.setPrimaryToMissingEntry());
      return;
    }
    fail('An exception should be thrown.');
  });

  it('set primary to entry with disabled key status', function() {
    const key = createKey(/* opt_keyId = */ 0x12345678,
        /* opt_legacy = */ false, /* opt_enabled = */ false);
    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);

    const primary = primitiveSet.addPrimitive(new DummyAead1(), key);

    try {
      primitiveSet.setPrimary(primary);
    } catch (e: any) {
      expect(e.toString()).toBe(ExceptionText.setPrimaryToDisabled());
      return;
    }
    fail('An exception should be thrown.');
  });

  it('set and get primary', function() {
    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
    expect(primitiveSet.getPrimary()).toBe(null);

    const key1 = createKey(/* opt_keyId = */ 0xBBBCCC);

    const result = primitiveSet.addPrimitive(new DummyAead1(), key1);
    // Check that primary remains unset, set it to newly added and verify that
    // it was set.
    expect(primitiveSet.getPrimary()).toBe(null);
    primitiveSet.setPrimary(result);
    expect(primitiveSet.getPrimary()).toEqual(result);

    const key2 = createKey(/* opt_keyId = */ 0xAAABBB);
    // Add new primitive, check that it does not change primary.
    const result2 = primitiveSet.addPrimitive(new DummyAead2(), key2);
    expect(primitiveSet.getPrimary()).toEqual(result);

    // Change the primary and verify the change.
    primitiveSet.setPrimary(result2);
    expect(primitiveSet.getPrimary()).toEqual(result2);
  });

  it('set primary, raw primitives', function() {
    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
    for (let i = 0; i < 3; i++) {
      const key = createKey(i).setOutputPrefixType(PbOutputPrefixType.RAW);
      primitiveSet.addPrimitive(new DummyAead1(), key);
    }
    const primaryKey =
        createKey(255).setOutputPrefixType(PbOutputPrefixType.RAW);
    const primaryEntry =
        primitiveSet.addPrimitive(new DummyAead1(), primaryKey);
    primitiveSet.setPrimary(primaryEntry);
  });

  it('get primary type', function() {
    const primitiveSet = new PrimitiveSet.PrimitiveSet(Aead);
    expect(primitiveSet.getPrimitiveType()).toEqual(Aead);
  });
});

// Helper classes and functions used for testing purposes.

class ExceptionText {
  static unknownPrefixType(): string {
    return 'SecurityException: Unsupported key prefix type.';
  }

  static addingNullPrimitive(): string {
    return 'SecurityException: Primitive has to be non null.';
  }

  static addingNullKey(): string {
    return 'SecurityException: Key has to be non null.';
  }

  static setPrimaryToNull(): string {
    return 'SecurityException: Primary cannot be set to null.';
  }

  static setPrimaryToMissingEntry(): string {
    return 'SecurityException: Primary cannot be set to an entry which is ' +
        'not held by this primitive set.';
  }

  static setPrimaryToDisabled(): string {
    return 'SecurityException: Primary has to be enabled.';
  }
}

/** @final */
class DummyAead1 extends Aead {
  encrypt(plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
    throw new SecurityException(
        'Not implemented, intentended just for testing.');
  }

  decrypt(ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
    throw new SecurityException(
        'Not implemented, intentended just for testing.');
  }
}

/** @final */
class DummyAead2 extends Aead {
  encrypt(plaintext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
    throw new SecurityException(
        'Not implemented, intentended just for testing.');
  }

  decrypt(ciphertext: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {
    throw new SecurityException(
        'Not implemented, intentended just for testing.');
  }
}

/**
 * Function for creating primitive sets for testing purposes.
 * Returns a primitive set containing n values with keyIds from {0, .. , n-1}.
 * There are different combinations of
 *    primitive types ({DummyAead1, DummyAead2}),
 *    key status (enabled, disabled),
 *    and key types (legacy, tink).
 */
function initPrimitiveSet(n: number): PrimitiveSet.PrimitiveSet<Aead> {
  const primitiveSet = new PrimitiveSet.PrimitiveSet<Aead>(Aead);

  // Set primary.
  const primaryKey = createKey(/* opt_id = */ 0, /* opt_legacy = */ false,
      /* opt_enabled = */ true);
  const primary = primitiveSet.addPrimitive(new DummyAead1(), primaryKey);
  primitiveSet.setPrimary(primary);

  // Add n-1 other keys to primitive set.
  for (let id = 1; id < n; id++) {
    const legacyKeyType = ((id % 2) < 1);
    const enabledKey = ((id % 4) < 2);
    const key = createKey(id, legacyKeyType, enabledKey);

    let primitive;
    if ((id % 8) < 4) {
      primitive = new DummyAead1();
    } else {
      primitive = new DummyAead2();
    }

    primitiveSet.addPrimitive(primitive, key);
  }

  return primitiveSet;
}

/**
 * Function for creating keys for testing purposes.
 * If doesn't set otherwise it generates a key with id 0x12345678, which is
 * ENABLED and with prefix type TINK.
 */
function createKey(
    opt_keyId: number = 0x12345678, opt_legacy: boolean = false,
    opt_enabled: boolean = true): PbKeysetKey {
  const key = new PbKeysetKey();

  if (opt_enabled) {
    key.setStatus(PbKeyStatusType.ENABLED);
  } else {
    key.setStatus(PbKeyStatusType.DISABLED);
  }

  if (opt_legacy) {
    key.setOutputPrefixType(PbOutputPrefixType.LEGACY);
  } else {
    key.setOutputPrefixType(PbOutputPrefixType.TINK);
  }

  key.setKeyId(opt_keyId);

  return key;
}
