import {
  AbstractConfigDumpArgs,
  AbstractConfigLoadArgs,
  AbstractCryptoConfig,
  ArgumentsError,
  CryptoConfigVersioning,
  CryptoContext,
} from './context/base';
import { AESCipherCBC, AESCipherSIVNonce96 } from './base/cipher/aes';
import { Ed25519DS } from './base/ds/ec';
import { concatByteArray } from './utility';
import { base64 } from 'rfc4648';
import { Ed25519KeyGenerator } from './base/key/ec';

describe('Cryptography context-base test', () => {
  class _TestingLoadArgs extends AbstractConfigLoadArgs {
    public key;
    public assoc;
    public signing_pk;

    constructor(cipher: Uint8Array, key: Uint8Array, assoc?: Uint8Array, signing_pk?: Uint8Array) {
      super(cipher);
      this.key = key;
      this.assoc = assoc;
      this.signing_pk = signing_pk;
    }
  }
  class _TestingDumpArgs extends AbstractConfigDumpArgs {
    public plain;
    public key;
    public assoc;
    public signing_sk;

    constructor(plain: Uint8Array, key: Uint8Array, assoc?: Uint8Array, signing_sk?: Uint8Array) {
      super();
      this.plain = plain;
      this.key = key;
      this.assoc = assoc;
      this.signing_sk = signing_sk;
    }
  }
  class _TestingGroupContentConfigV1 extends AbstractCryptoConfig<
    _TestingLoadArgs,
    _TestingDumpArgs
  > {
    constructor(never_dump: boolean = false) {
      super(1, never_dump);
    }

    public load(args: _TestingLoadArgs): Promise<Uint8Array> {
      return AESCipherCBC.decrypt(args.cipher.slice(this.version.byteLength), args.key);
    }

    public dump(args: _TestingDumpArgs): Promise<Uint8Array> {
      if (args.assoc !== undefined || args.signing_sk !== undefined) throw new ArgumentsError();
      return AESCipherCBC.encrypt(args.plain, args.key, undefined, this.version);
    }
  }
  class _TestingGroupContentConfigV2 extends AbstractCryptoConfig<
    _TestingLoadArgs,
    _TestingDumpArgs
  > {
    constructor(never_dump: boolean = false) {
      super(2, never_dump);
    }

    public load(args: _TestingLoadArgs): Promise<Uint8Array> {
      return AESCipherSIVNonce96.decrypt(
        args.cipher.slice(this.version.byteLength),
        args.key,
        args.assoc === undefined ? new Uint8Array([]) : args.assoc
      );
    }

    public dump(args: _TestingDumpArgs): Promise<Uint8Array> {
      if (args.key.byteLength < 32) throw new ArgumentsError(); // to be able to fall back to V1
      return AESCipherSIVNonce96.encrypt(
        args.plain,
        args.key,
        undefined,
        args.assoc === undefined ? new Uint8Array([]) : args.assoc,
        this.version
      );
    }
  }
  class _TestingGroupContentConfigV3 extends AbstractCryptoConfig<
    _TestingLoadArgs,
    _TestingDumpArgs
  > {
    constructor(never_dump: boolean = false) {
      super(3, never_dump);
    }

    public load(args: _TestingLoadArgs): Promise<Uint8Array> {
      return AESCipherSIVNonce96.decrypt(
        args.cipher.slice(this.version.byteLength),
        args.key,
        args.assoc === undefined ? new Uint8Array([]) : args.assoc
      );
    }

    public dump(args: _TestingDumpArgs): Promise<Uint8Array> {
      if (args.key.byteLength < 32) throw new ArgumentsError(); // to be able to fall back to V1
      return AESCipherSIVNonce96.encrypt(
        args.plain,
        args.key,
        undefined,
        args.assoc === undefined ? new Uint8Array([]) : args.assoc,
        this.version
      );
    }
  }
  class _TestingGroupContentConfigV4 extends AbstractCryptoConfig<
    _TestingLoadArgs,
    _TestingDumpArgs
  > {
    constructor(never_dump: boolean = false) {
      super(4, never_dump);
    }

    public load(args: _TestingLoadArgs): Promise<Uint8Array> {
      return Ed25519DS.verify(
        args.cipher.slice(this.version.byteLength, this.version.byteLength + 64),
        args.signing_pk,
        args.cipher.slice(this.version.byteLength + 64)
      ).then(() =>
        AESCipherSIVNonce96.decrypt(
          args.cipher.slice(this.version.byteLength + 64),
          args.key,
          args.assoc === undefined ? new Uint8Array([]) : args.assoc
        )
      );
    }

    public dump(args: _TestingDumpArgs): Promise<Uint8Array> {
      if (args.key.byteLength < 32) throw new ArgumentsError(); // to be able to fall back to V1
      if (args.signing_sk === undefined) throw new ArgumentsError(); // to be able to fall back to V3
      return AESCipherSIVNonce96.encrypt(
        args.plain,
        args.key,
        undefined,
        args.assoc === undefined ? new Uint8Array([]) : args.assoc
      ).then((cipher) => {
        return Ed25519DS.sign(cipher, args.signing_sk).then((signature) => {
          return concatByteArray(this.version, signature, cipher);
        });
      });
    }
  }

  class _TestingCryptoContext extends CryptoContext<_TestingLoadArgs, _TestingDumpArgs> {}

  class _ContextTestingStore {
    private _stored: Uint8Array[] = [];
    private _plain: Uint8Array[] = [];
    private _key: Uint8Array[] = [];
    private _assoc: Uint8Array[] = [];
    private _spk: Uint8Array[] = [];

    constructor() {}

    public async dump(
      c: _TestingCryptoContext,
      plain: Uint8Array,
      key: Uint8Array,
      assoc?: Uint8Array,
      signing_sk?: Uint8Array,
      signing_pk?: Uint8Array
    ): Promise<void> {
      expect(signing_sk === undefined).toEqual(signing_pk === undefined);
      let data = await c.dump(
        new _TestingDumpArgs(
          plain,
          key,
          assoc,
          signing_sk === undefined ? undefined : concatByteArray(signing_sk, signing_pk)
        )
      );
      this._stored.push(data);
      this._plain.push(plain);
      this._key.push(key);
      this._assoc.push(assoc);
      this._spk.push(signing_pk);
    }

    public async check_load_all(c: _TestingCryptoContext): Promise<void> {
      for (let i = 0; i < this._stored.length; i++) {
        let tmp = await c.load(
          new _TestingLoadArgs(this._stored[i], this._key[i], this._assoc[i], this._spk[i])
        );
        expect(tmp).toEqual(this._plain[i]);
      }
    }

    public stored_config_versions(): Array<number> {
      let a = [];
      for (let i = 0; i < this._stored.length; i++) {
        a.push(
          CryptoConfigVersioning.config_version_to_int(
            CryptoConfigVersioning.config_version_from_data(this._stored[i])
          )
        );
      }
      return a;
    }
  }

  function urandom(n: number): Uint8Array {
    return window.crypto.getRandomValues(new Uint8Array(n));
  }

  it('test context crypto legacy versioning', async () => {
    const v1 = new _TestingGroupContentConfigV1();
    const v2 = new _TestingGroupContentConfigV2();
    let c = new _TestingCryptoContext([v1, v2]);

    const k1 = urandom(16);
    const p1 = new Uint8Array([
      102, 105, 114, 115, 116, 45, 112, 97, 121, 108, 111, 97, 100, 45, 110, 111,
    ]);
    const ct1 = await c.dump(new _TestingDumpArgs(p1, k1));
    expect(base64.stringify(ct1.slice(0, 1))).toEqual(base64.stringify(v1.version));
    expect(ct1.byteLength).toEqual(p1.byteLength + 33); // TODO !!! NOTE this is stupid (thanks web-crypto?)
    const k2 = urandom(32);
    const p2 = new Uint8Array([
      115, 101, 99, 111, 110, 100, 45, 112, 97, 121, 108, 111, 97, 100, 45, 63,
    ]);
    const ct2 = await c.dump(new _TestingDumpArgs(p2, k2));
    expect(base64.stringify(ct2.slice(0, 1))).toEqual(base64.stringify(v2.version));
    expect(ct2.length).toEqual(p2.length + 29);

    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct1, k1)))).toEqual(
      base64.stringify(p1)
    );
    await expectAsync(c.load(new _TestingLoadArgs(ct1.slice(1), k1))).toBeRejected();
    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct2, k2)))).toEqual(
      base64.stringify(p2)
    );
    await expectAsync(c.load(new _TestingLoadArgs(ct2.slice(1), k2))).toBeRejected();

    c = new _TestingCryptoContext([v1, v2], [v1.version]);
    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct1, k1)))).toEqual(
      base64.stringify(p1)
    );
    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct1.slice(1), k1)))).toEqual(
      base64.stringify(p1)
    );
    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct2, k2)))).toEqual(
      base64.stringify(p2)
    );
    await expectAsync(c.load(new _TestingLoadArgs(ct2.slice(1), k2))).toBeRejected();

    c = new _TestingCryptoContext([v1, v2], [v2.version]);
    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct1, k1)))).toEqual(
      base64.stringify(p1)
    );
    await expectAsync(c.load(new _TestingLoadArgs(ct1.slice(1), k1))).toBeRejected();
    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct2, k2)))).toEqual(
      base64.stringify(p2)
    );
    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct2.slice(1), k2)))).toEqual(
      base64.stringify(p2)
    );

    c = new _TestingCryptoContext([v1, v2], [v1.version, v2.version]);
    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct1, k1)))).toEqual(
      base64.stringify(p1)
    );
    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct1.slice(1), k1)))).toEqual(
      base64.stringify(p1)
    );
    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct2, k2)))).toEqual(
      base64.stringify(p2)
    );
    expect(base64.stringify(await c.load(new _TestingLoadArgs(ct2.slice(1), k2)))).toEqual(
      base64.stringify(p2)
    );

    expect(() => new _TestingCryptoContext([v1, v2], [new Uint8Array([190])])).toThrow();
  });

  it('test context crypto versioning', async () => {
    expect(
      () =>
        new _TestingCryptoContext([
          new _TestingGroupContentConfigV1(),
          new _TestingGroupContentConfigV1(),
        ])
    ).toThrow('Config version conflict for context');

    const cs = new _ContextTestingStore();

    let c = new _TestingCryptoContext([new _TestingGroupContentConfigV1()]);
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(c.top_config_version()));
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(new Uint8Array([1])));
    await cs.dump(c, urandom(16), urandom(16));
    await cs.check_load_all(c);
    expect(cs.stored_config_versions()).toEqual([1]);
    await expectAsync(cs.dump(c, urandom(16), urandom(8))).toBeRejected();

    c = new _TestingCryptoContext([
      new _TestingGroupContentConfigV1(),
      new _TestingGroupContentConfigV2(),
    ]);
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(c.top_config_version()));
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(new Uint8Array([2])));
    await cs.dump(c, urandom(16), urandom(16));
    await cs.check_load_all(c);
    expect(cs.stored_config_versions()).toEqual([1, 1]);
    await cs.dump(c, urandom(16), urandom(32));
    await cs.check_load_all(c);
    expect(cs.stored_config_versions()).toEqual([1, 1, 2]);

    c = new _TestingCryptoContext([
      new _TestingGroupContentConfigV1(),
      new _TestingGroupContentConfigV2(),
      new _TestingGroupContentConfigV3(),
    ]);
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(c.top_config_version()));
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(new Uint8Array([3])));
    await cs.dump(c, urandom(16), urandom(16));
    await expectAsync(cs.dump(c, urandom(16), urandom(16), urandom(4))).toBeRejected();
    await cs.dump(c, urandom(16), urandom(32));
    await cs.check_load_all(c);
    expect(cs.stored_config_versions()).toEqual([1, 1, 2, 1, 3]);

    c = new _TestingCryptoContext([
      new _TestingGroupContentConfigV1(true),
      new _TestingGroupContentConfigV2(true),
      new _TestingGroupContentConfigV3(),
    ]);
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(c.top_config_version()));
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(new Uint8Array([3])));
    await expectAsync(cs.dump(c, urandom(16), urandom(16))).toBeRejected();
    await expectAsync(cs.dump(c, urandom(16), urandom(16), urandom(4))).toBeRejected();
    await cs.dump(c, urandom(16), urandom(32));
    await cs.check_load_all(c);
    expect(cs.stored_config_versions()).toEqual([1, 1, 2, 1, 3, 3]);

    const kp = Ed25519KeyGenerator.new_key();
    c = new _TestingCryptoContext([
      new _TestingGroupContentConfigV1(true),
      new _TestingGroupContentConfigV2(true),
      new _TestingGroupContentConfigV3(),
      new _TestingGroupContentConfigV4(true),
    ]);
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(c.top_config_version()));
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(new Uint8Array([4])));
    await cs.dump(c, urandom(16), urandom(32), undefined, kp.secretKey, kp.publicKey);
    await cs.check_load_all(c);
    expect(cs.stored_config_versions()).toEqual([1, 1, 2, 1, 3, 3, 3]);
    await cs.dump(c, urandom(16), urandom(32));
    await cs.check_load_all(c);
    expect(cs.stored_config_versions()).toEqual([1, 1, 2, 1, 3, 3, 3, 3]);

    c = new _TestingCryptoContext([
      new _TestingGroupContentConfigV1(true),
      new _TestingGroupContentConfigV2(true),
      new _TestingGroupContentConfigV3(),
      new _TestingGroupContentConfigV4(),
    ]);
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(c.top_config_version()));
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(new Uint8Array([4])));
    await cs.dump(c, urandom(16), urandom(32), undefined, kp.secretKey, kp.publicKey);
    await cs.check_load_all(c);
    expect(cs.stored_config_versions()).toEqual([1, 1, 2, 1, 3, 3, 3, 3, 4]);
    await cs.dump(c, urandom(16), urandom(32));
    await cs.check_load_all(c);
    expect(cs.stored_config_versions()).toEqual([1, 1, 2, 1, 3, 3, 3, 3, 4, 3]);

    c = new _TestingCryptoContext([
      new _TestingGroupContentConfigV1(true),
      new _TestingGroupContentConfigV2(true),
      new _TestingGroupContentConfigV3(true),
      new _TestingGroupContentConfigV4(),
    ]);
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(c.top_config_version()));
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(new Uint8Array([4])));
    await cs.dump(c, urandom(16), urandom(32), undefined, kp.secretKey, kp.publicKey);
    await cs.check_load_all(c);
    expect(cs.stored_config_versions()).toEqual([1, 1, 2, 1, 3, 3, 3, 3, 4, 3, 4]);
    await expectAsync(cs.dump(c, urandom(16), urandom(16))).toBeRejected();
    await expectAsync(cs.dump(c, urandom(16), urandom(32))).toBeRejected();
    await cs.check_load_all(c);
    expect(cs.stored_config_versions()).toEqual([1, 1, 2, 1, 3, 3, 3, 3, 4, 3, 4]);

    c = new _TestingCryptoContext([
      new _TestingGroupContentConfigV2(true),
      new _TestingGroupContentConfigV3(true),
      new _TestingGroupContentConfigV4(),
    ]);
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(c.top_config_version()));
    expect(base64.stringify(c.top_config()[0])).toEqual(base64.stringify(new Uint8Array([4])));
    await expectAsync(cs.check_load_all(c)).toBeRejected();

    expect(c.get_config(1)).toBeFalsy();
    expect(base64.stringify(c.get_config(3).version)).toEqual(
      base64.stringify(new Uint8Array([3]))
    );
  });
});
