/*
 * Copyright (C) Sapphirecode - All Rights Reserved
 * This file is part of Auth-Server-Helper which is released under MIT.
 * See file 'LICENSE' for full license details.
 * Created by Timo Hocker <timo@scode.ovh>, December 2020
 */

import ks, { KeyStore } from '../../lib/KeyStore';
import { clock_finalize, clock_setup } from '../Helper';

const frame = 3600;

/* eslint-disable-next-line max-lines-per-function */
describe ('key store', () => {
  beforeAll (() => {
    clock_setup ();
  });

  afterAll (() => {
    clock_finalize ();
  });

  const keys: {key: string, sign: string, iat: number}[] = [];

  it ('should generate a new key', async () => {
    const iat = (new Date)
      .getTime () / 1000;
    const duration = 10 * frame;
    const key = await ks.get_sign_key (iat, duration);
    const sign = await ks.get_key (iat);
    expect (typeof key)
      .toEqual ('string');
    expect (typeof sign)
      .toEqual ('string');
    keys.push ({ iat, key, sign });
  });

  it ('should return the generated key', async () => {
    const key = await ks.get_sign_key (keys[0].iat, 1);
    expect (key)
      .toEqual (keys[0].key);
    const sign = await ks.get_key (keys[0].iat);
    expect (sign)
      .toEqual (keys[0].sign);
  });

  it ('should return the same key on a different time', async () => {
    const key = await ks.get_sign_key (keys[0].iat + (frame / 2), 1);
    expect (key)
      .toEqual (keys[0].key);
    const sign = await ks.get_key (keys[0].iat + (frame / 2));
    expect (sign)
      .toEqual (keys[0].sign);
  });

  it ('should generate a new key after time frame is over', async () => {
    jasmine.clock ()
      .tick (frame * 1000);
    const iat = (new Date)
      .getTime () / 1000;
    const duration = 10 * frame;
    const key = await ks.get_sign_key (iat, duration);
    const sign = await ks.get_key (iat);
    expect (typeof key)
      .toEqual ('string');
    expect (key).not.toEqual (keys[0].key);
    expect (sign).not.toEqual (keys[0].sign);
    keys.push ({ iat, key, sign });
  });

  it ('should return both keys, but not the first sign key', async () => {
    const sign = await ks.get_key (keys[0].iat);
    expect (sign)
      .toEqual (keys[0].sign);
    await expectAsync (ks.get_sign_key (keys[0].iat, 1))
      .toBeRejectedWithError ('cannot access already expired keys');
    const k2 = await ks.get_sign_key (keys[1].iat, 1);
    const s2 = await ks.get_key (keys[1].iat);
    expect (k2)
      .toEqual (keys[1].key);
    expect (s2)
      .toEqual (keys[1].sign);
  });

  it ('should throw on non existing key', async () => {
    await expectAsync (ks.get_key (keys[1].iat + frame))
      .toBeRejectedWithError ('key could not be found');
  });

  it ('should delete a key after it expires', async () => {
    // go to 10 frames + 1ms after key creation
    jasmine.clock ()
      .tick ((frame * 9e3) + 1);
    // eslint-disable-next-line dot-notation
    ks['garbage_collect'] ();
    await expectAsync (ks.get_key (keys[0].iat))
      .toBeRejectedWithError ('key could not be found');
  });

  it (
    'should still retrieve the second key, but not its sign key',
    async () => {
      await expectAsync (ks.get_sign_key (keys[1].iat, 1))
        .toBeRejectedWithError ('cannot access already expired keys');
      const sign = await ks.get_key (keys[1].iat);
      expect (sign)
        .toEqual (keys[1].sign);
    }
  );

  it ('should reject key generation of expired keys', async () => {
    const iat = ((new Date)
      .getTime () / 1000) - 2;
    const duration = 5;
    await expectAsync (ks.get_sign_key (iat, duration))
      .toBeRejectedWithError ('cannot access already expired keys');
  });

  it ('key should live as long as the longest created token', async () => {
    jasmine.clock ()
      .tick (frame * 10e3);
    const iat = (new Date)
      .getTime () / 1000;
    const duration1 = frame;
    const duration2 = frame * 10;

    const key1 = await ks.get_sign_key (iat, duration1);
    const step = 0.9 * frame;
    jasmine.clock ()
      .tick (step * 1000);
    const key2 = await ks.get_sign_key (iat + step, duration2);
    const sign = await ks.get_key (iat);
    expect (key1)
      .toEqual (key2);
    jasmine.clock ()
      .tick (5000 * frame);
    const signv = await ks.get_key (iat + step);
    expect (signv)
      .toEqual (sign);
  });

  it ('should not allow invalid expiry times', async () => {
    await expectAsync (ks.get_sign_key (0, 0))
      .toBeRejectedWithError ('cannot create infinitely valid key');
    await expectAsync (ks.get_sign_key (0, -1))
      .toBeRejectedWithError ('cannot create infinitely valid key');
  });

  it ('should export and import all keys', async () => {
    const iat = (new Date)
      .getTime () / 1000;

    const sign = await ks.get_sign_key (iat, frame);
    const ver = await ks.get_key (iat);
    const exp = ks.export_verification_data ();
    // eslint-disable-next-line dot-notation
    expect (Object.keys (ks['_keys']))
      .toEqual (exp.map ((v) => v.index));

    const ks2 = (new KeyStore);
    expect (ks2.instance_id).not.toEqual (ks.instance_id);
    ks2.import_verification_data (exp);
    // eslint-disable-next-line dot-notation
    expect (Object.keys (ks2['_keys']))
      .toEqual (exp.map ((v) => v.index));

    const sign2 = await ks2.get_sign_key (iat, frame);
    const ver2 = await ks2.get_key (iat);
    expect (sign).not.toEqual (sign2);
    expect (ver).not.toEqual (ver2);
    await expectAsync (ks2.get_sign_key (iat, 60, ks.instance_id))
      .toBeRejectedWithError ('cannot access already expired keys');
    expect (await ks2.get_key (iat, ks.instance_id))
      .toEqual (ver);
  });

  it ('should disallow importing to itself', () => {
    const exp = ks.export_verification_data ();
    expect (() => ks.import_verification_data (exp))
      .toThrowError ('cannot import to the same instance');
  });

  it ('should clear all', () => {
    // eslint-disable-next-line dot-notation
    expect (Object.keys (ks['_keys']).length)
      .toBeGreaterThan (0);
    const instance = ks.instance_id;
    ks.reset_instance ();
    // eslint-disable-next-line dot-notation
    expect (Object.keys (ks['_keys']).length)
      .toEqual (0);
    expect (instance).not.toEqual (ks.instance_id);
  });
});