import { TestBed, waitForAsync } from '@angular/core/testing';
import { HttpClient, HttpHeaders, HttpClientModule } from '@angular/common/http';
import * as miscreant from 'miscreant';
// npm install git+https://github.com/Traktormaster/miscreant.js --save
import { CookieService } from 'ngx-cookie';
import { base64 } from 'rfc4648';
import * as nacl from 'tweetnacl';
import { CookieModule } from 'ngx-cookie';

import {
  AESCipherSIVDeterministic,
  AESCipherSIVNonce96,
  AESCipherGCM,
  AESCipherCBC,
} from '../crypto/base/cipher/aes';
import { Ed25519KeyGenerator, Curve25519KeyGenerator } from '../crypto/base/key/ec';
import { Curve25519SharedSecretGenerator } from '../crypto/base/key/dh';
import {
  HKDFKeyGeneratorV1,
  HKDFKeyGeneratorV2,
  AbstractHKDFKeyGenerator,
} from '../crypto/base/key/kdf';
import {
  RandomKeyGenerator128,
  RandomKeyGenerator256,
  RandomKeyGenerator512,
} from '../crypto/base/key/random';
import { Ed25519DS } from '../crypto/base/ds/ec';
import msgpack from 'msgpack-lite';
import { concatByteArray } from './utility';
import { environment } from 'src/environments/environment';
import { GroupConfigBlockChain } from './group_config/chain';
import { PrivateKeyringV1 } from './keyring/private';

/**
 *
 * THIS FILE IS TESTING THE BOTTOM/NATIVE LAYER OF OUR CRYPTO FUNCIONALITY
 * WE MADE A HANDY INTERFACE FOR THIS TO USE: global-crypto.ts
 *
 */

describe('Cryptography base test', () => {
  let cookie;
  let conf: any = {};

  let options = {
    headers: new HttpHeaders({ 'Content-type': 'application/msgpack' }),
    responseType: 'arraybuffer' as 'arraybuffer',
  };

  let http: HttpClient;
  let apiUrl = environment.url_api + '/';

  let expectedList = [
    'AES-CBC-128/256',
    'AES-GCM-128/256',
    'AES-SIV-N0-128/256',
    'AES-SIV-N96-128/256',
    'Curve25519-256',
    'Curve25519-ECDH',
    'Ed25519-256',
    'Ed25519-DS-256',
    'HKDF-SHA256',
    'HKDF-SHA512',
    'Random-128',
    'Random-256',
    'Random-512',
  ];

  let csrfToken = '';

  beforeAll((done) => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule, CookieModule.forRoot()],
      providers: [CookieService],
    });

    http = TestBed.inject(HttpClient);
    cookie = TestBed.inject(CookieService);

    http.get(apiUrl + 'debug/crypto/base/configs', options).subscribe((res) => {
      conf = msgpack.decode(new Uint8Array(res));

      // set the cookie
      http.get(apiUrl + 'r/csrf', options).subscribe((res) => {
        csrfToken = cookie.get('__Secure-csrf');
        console.log('csrfToken', csrfToken);
        options = {
          headers: new HttpHeaders({
            'Content-type': 'application/msgpack',
            pcsrf: csrfToken,
          }),
          responseType: 'arraybuffer' as 'arraybuffer',
        };

        done();
      });
    });
  });

  // check if we can get the conf list from server
  it('should get server conf', () => {
    console.log(conf);
    expect(conf).toBeTruthy();
  });

  // if this arr is changed, we should write more tests for the new elements
  it('check the conf values', () => {
    expect(expectedList.join() == conf['cbs'].join()).toBeTrue();
  });

  // if this arr is changed, we should write more tests for the new elements
  it('check the const values', (done) => {
    http
      .get(apiUrl + 'debug/crypto/constants', options)
      .toPromise()
      .then((res) => {
        let expectedConsts: {
          'chain.BLOCK_REPLACE_MINIMUM_AGE': number;
          'chain.MINIMUM_BLOCKS_TO_REPLACE': number;
          default_private_keyring_version: string;
        } = msgpack.decode(new Uint8Array(res));

        expect(
          expectedConsts['chain.BLOCK_REPLACE_MINIMUM_AGE'] ==
            GroupConfigBlockChain.BLOCK_REPLACE_MINIMUM_AGE
        ).toBeTrue();
        expect(
          expectedConsts['chain.MINIMUM_BLOCKS_TO_REPLACE'] ==
            GroupConfigBlockChain.MINIMUM_BLOCKS_TO_REPLACE
        ).toBeTrue();
        expect(
          expectedConsts.default_private_keyring_version == PrivateKeyringV1.VERSION
        ).toBeTrue();

        done();
      });
  });

  // AES-SIV-N0-128/256 -------------------------------------------------------------------------------------
  it('check the AES-SIV-N0-128/256', (done) => {
    // https://dc.clarabot.com/diffusion/7/browse/master/clarabot_crypto/base/cipher/aes.py$46
    // def encrypt(plain: bytes, key: bytes, assoc: Optional[bytes] = None) -> bytes:
    // def decrypt(cipher: bytes, key: bytes, assoc: Optional[bytes] = None) -> bytes:

    let key = window.crypto.getRandomValues(new Uint8Array(32));
    let other_key = window.crypto.getRandomValues(new Uint8Array(32));
    let plain = window.crypto.getRandomValues(new Uint8Array(512));

    let param = msgpack.encode({
      name: 'AES-SIV-N0-128/256',
      args: [plain, key],
    });

    // DEV encrypt call
    http
      .post(apiUrl + 'debug/crypto/base/dump', new Uint8Array(param).buffer, options)
      .toPromise()
      .then((res) => {
        let expectedEncryptedPlain = msgpack.decode(new Uint8Array(res));

        AESCipherSIVDeterministic.encrypt(plain, key).then((encrypted) => {
          expect(
            base64.stringify(expectedEncryptedPlain.data) == base64.stringify(encrypted)
          ).toBeTrue();

          // negative test
          AESCipherSIVDeterministic.decrypt(encrypted, other_key)
            .then((res) => {
              expect(false).toBeTrue();
            })
            .catch((err) => {
              expect(err).toEqual('Error: AES-SIV: ciphertext verification failure!');

              AESCipherSIVDeterministic.decrypt(encrypted, key).then((decrypted) => {
                expect(base64.stringify(decrypted) == base64.stringify(plain)).toBeTrue();
                done();
              });
            });
        });
      });
  });

  // AES-SIV-N96-128/256 -------------------------------------------------------------------------------------
  it('check the AES-SIV-N96-128/256', (done) => {
    // https://dc.clarabot.com/diffusion/7/browse/master/clarabot_crypto/base/cipher/aes.py$9
    // def encrypt(plain: bytes, key: bytes, nonce: Optional[bytes] = None, assoc: Optional[bytes] = None) -> bytes:
    // def decrypt(cipher: bytes, key: bytes, assoc: Optional[bytes] = None) -> bytes:

    let key = window.crypto.getRandomValues(new Uint8Array(32));
    let plain = window.crypto.getRandomValues(new Uint8Array(512));
    let nonce = window.crypto.getRandomValues(new Uint8Array(12));

    let param = msgpack.encode({
      name: 'AES-SIV-N96-128/256',
      args: [plain, key, nonce],
    });

    // DEV encrypt call
    http
      .post(apiUrl + 'debug/crypto/base/dump', new Uint8Array(param).buffer, options)
      .toPromise()
      .then((res) => {
        let expectedEncryptedPlain = msgpack.decode(new Uint8Array(res));

        AESCipherSIVNonce96.encrypt(plain, key, nonce).then((encrypted) => {
          expect(
            base64.stringify(expectedEncryptedPlain.data) == base64.stringify(encrypted)
          ).toBeTrue();

          AESCipherSIVNonce96.decrypt(encrypted, key).then((decrypted) => {
            expect(base64.stringify(decrypted) == base64.stringify(plain)).toBeTrue();
            done();
          });
        });
      });
  });

  // AES-GCM-128/256 -------------------------------------------------------------------------------------
  it('check the AES-GCM-128/256', (done) => {
    // https://dc.clarabot.com/diffusion/7/browse/master/clarabot_crypto/base/cipher/aes.py$9
    // def encrypt(plain: bytes, key: bytes, nonce: Optional[bytes] = None, assoc: Optional[bytes] = None) -> bytes:
    // def decrypt(cipher: bytes, key: bytes, assoc: Optional[bytes] = None) -> bytes:

    let key = window.crypto.getRandomValues(new Uint8Array(32));
    let plain = window.crypto.getRandomValues(new Uint8Array(512));
    let nonce = window.crypto.getRandomValues(new Uint8Array(12));

    let param = msgpack.encode({
      name: 'AES-GCM-128/256',
      args: [plain, key, nonce],
    });

    // DEV encrypt call
    http
      .post(apiUrl + 'debug/crypto/base/dump', new Uint8Array(param).buffer, options)
      .toPromise()
      .then((res) => {
        let expectedEncryptedPlain = msgpack.decode(new Uint8Array(res));

        AESCipherGCM.encrypt(plain, key, nonce).then((encrypted) => {
          // TODO after release, we can check this
          //expect(base64.stringify(expectedEncryptedPlain.data) == base64.stringify(encrypted)).toBeTrue();

          AESCipherGCM.decrypt(encrypted, key).then(
            (decrypted) => {
              expect(base64.stringify(decrypted) == base64.stringify(plain)).toBeTrue();
              done();
            },
            (err) => {
              console.log('err', err);
            }
          );
        });
      });
  });

  // AES-CBC-128/256 -------------------------------------------------------------------------------------
  it('check the AES-CBC-128/256', (done) => {
    // https://dc.clarabot.com/diffusion/7/browse/master/clarabot_crypto/base/cipher/aes.py$9
    // def encrypt(plain: bytes, key: bytes, nonce: Optional[bytes] = None, assoc: Optional[bytes] = None) -> bytes:
    // def decrypt(cipher: bytes, key: bytes, assoc: Optional[bytes] = None) -> bytes:

    let key = window.crypto.getRandomValues(new Uint8Array(32));
    let plain = window.crypto.getRandomValues(new Uint8Array(512));
    let iv = window.crypto.getRandomValues(new Uint8Array(16));

    let param = msgpack.encode({
      name: 'AES-CBC-128/256',
      args: [plain, key, iv],
    });

    // DEV encrypt call
    http
      .post(apiUrl + 'debug/crypto/base/dump', new Uint8Array(param).buffer, options)
      .toPromise()
      .then((res) => {
        let expectedEncryptedPlain = msgpack.decode(new Uint8Array(res));

        AESCipherCBC.encrypt(plain, key, iv).then((encrypted) => {
          //TODO our crypto has +16 bytes, but why? padding what
          //expect(base64.stringify(expectedEncryptedPlain.data) == base64.stringify(encrypted)).toBeTrue();
          //console.log('check', expectedEncryptedPlain.data, encrypted)

          AESCipherCBC.decrypt(encrypted, key).then(
            (decrypted) => {
              expect(base64.stringify(decrypted) == base64.stringify(plain)).toBeTrue();
              done();
            },
            (err) => {
              console.log('err', err);
            }
          );
        });
      });
  });

  // Ed25519-256 -------------------------------------------------------------------------------------
  /**
   * we have a seed from the server
   * generate a public/secret key from the seed
   * sign the msg with the secretKey, as a result we have a signature
   * we can check the signed msg with (msg, publicKey, signature), true if its ok
   */
  it('check the Ed25519-256', (done) => {
    let param = msgpack.encode({
      name: 'Ed25519-256',
    });

    http
      .post(apiUrl + 'debug/crypto/base/gen-key', new Uint8Array(param).buffer, options)
      .toPromise()
      .then((res) => {
        let msg = msgpack.decode(new Uint8Array(res));

        let publicKey = msg['public_key'];
        let secretKeySeed = msg['secret_key']; // its not the key, its just the seed, we have to generate key from this with nacl.sign.keyPair.fromSeed(seed)
        let plain = window.crypto.getRandomValues(new Uint8Array(512));

        // check nacl regenerator
        let pair = Ed25519KeyGenerator.regenerate_key_pair(secretKeySeed);

        expect(base64.stringify(pair.publicKey) == base64.stringify(publicKey)).toBeTrue();

        let secretKey = pair['secretKey'];
        Ed25519DS.sign(plain, concatByteArray(secretKey, publicKey)).then((signatureNacl) => {
          Ed25519DS.verify(signatureNacl, publicKey, plain).then((verified) => {
            expect(base64.stringify(verified) == base64.stringify(plain)).toBeTrue();

            let param = msgpack.encode({
              name: 'Ed25519-DS-256',
              args: [plain, concatByteArray(secretKeySeed, publicKey)],
            });

            http
              .post(apiUrl + 'debug/crypto/base/dump', new Uint8Array(param).buffer, options)
              .toPromise()
              .then((res) => {
                let msg = msgpack.decode(new Uint8Array(res));

                let signature = msg['data'];

                expect(base64.stringify(signatureNacl) == base64.stringify(signature)).toBeTrue();

                let param = msgpack.encode({
                  name: 'Ed25519-DS-256',
                  args: [signature, publicKey, plain],
                });

                http
                  .post(apiUrl + 'debug/crypto/base/load', new Uint8Array(param).buffer, options)
                  .toPromise()
                  .then((res) => {
                    let msg = msgpack.decode(new Uint8Array(res));

                    let openedPlain = msg['plain'];

                    expect(base64.stringify(openedPlain) == base64.stringify(plain)).toBeTrue();

                    done();
                  });
              });
          });
        });
      });
  });

  // Curve25519-256 -------------------------------------------------------------------------------------
  /**
   * A and B generate a publicKey, secretKey
   * they make the same sharedSecret with fn(publicKeyA, secretKeyB) == fn(publicKeyB, secretKeyA)
   */
  it('check the Curve25519-256', (done) => {
    let param = msgpack.encode({
      name: 'Curve25519-256',
    });

    http
      .post(apiUrl + 'debug/crypto/base/gen-key', new Uint8Array(param).buffer, options)
      .toPromise()
      .then((res) => {
        let msg = msgpack.decode(new Uint8Array(res));

        let publicKeyA = msg['public_key'];
        let secretKeyA = msg['secret_key'];

        // test the regenerator
        let pair = Curve25519KeyGenerator.regenerate_key_pair(secretKeyA);

        expect(base64.stringify(pair.publicKey) == base64.stringify(publicKeyA)).toBeTrue();

        let param = msgpack.encode({
          name: 'Curve25519-256',
        });

        http
          .post(apiUrl + 'debug/crypto/base/gen-key', new Uint8Array(param).buffer, options)
          .toPromise()
          .then((res) => {
            let msg = msgpack.decode(new Uint8Array(res));

            let publicKeyB = msg['public_key'];
            let secretKeyB = msg['secret_key'];

            let sharedA = Curve25519SharedSecretGenerator.new_key(secretKeyA, publicKeyB);
            let sharedB = Curve25519SharedSecretGenerator.new_key(secretKeyB, publicKeyA);

            // check if share algorithm is working
            expect(base64.stringify(sharedA) == base64.stringify(sharedB)).toBeTrue();

            let param = msgpack.encode({
              name: 'Curve25519-ECDH',
              args: [secretKeyA, publicKeyB],
            });

            http
              .post(apiUrl + 'debug/crypto/base/gen-key', new Uint8Array(param).buffer, options)
              .toPromise()
              .then((res) => {
                let msg = msgpack.decode(new Uint8Array(res));

                let shared = msg['key'];

                expect(base64.stringify(sharedA) == base64.stringify(shared));

                let param = msgpack.encode({
                  name: 'Curve25519-ECDH',
                  args: [secretKeyB, publicKeyA],
                });
                http
                  .post(apiUrl + 'debug/crypto/base/gen-key', new Uint8Array(param).buffer, options)
                  .toPromise()
                  .then((res) => {
                    let msg = msgpack.decode(new Uint8Array(res));

                    let shared = msg['key'];

                    expect(base64.stringify(sharedB) == base64.stringify(shared));

                    done();
                  });
              });
          });
      });
  });

  // HKDF-SHA256 -------------------------------------------------------------------------------------
  it('check the HKDF-SHA256', (done) => {
    let rawKey = window.crypto.getRandomValues(new Uint8Array(32));
    let salt = AbstractHKDFKeyGenerator.SALTS[0];

    HKDFKeyGeneratorV1.new_key(32, rawKey, salt).then((deriveBits) => {
      let param = msgpack.encode({
        name: 'HKDF-SHA256',
        args: [32, rawKey, salt, null],
      });

      http
        .post(apiUrl + 'debug/crypto/base/gen-key', new Uint8Array(param).buffer, options)
        .toPromise()
        .then((res) => {
          let msg = msgpack.decode(new Uint8Array(res));

          let serverKey = msg['key'];

          expect(
            base64.stringify(new Uint8Array(deriveBits)) == base64.stringify(serverKey)
          ).toBeTrue();
          done();
        });
    });
  });

  // HKDF-SHA512 -------------------------------------------------------------------------------------
  it('check the HKDF-SHA512', (done) => {
    let rawKey = window.crypto.getRandomValues(new Uint8Array(32));
    let salt = AbstractHKDFKeyGenerator.SALTS[0];

    HKDFKeyGeneratorV2.new_key(32, rawKey, salt).then((deriveBits) => {
      let param = msgpack.encode({
        name: 'HKDF-SHA512',
        args: [32, rawKey, salt, null],
      });

      http
        .post(apiUrl + 'debug/crypto/base/gen-key', new Uint8Array(param).buffer, options)
        .toPromise()
        .then((res) => {
          let msg = msgpack.decode(new Uint8Array(res));

          let serverKey = msg['key'];

          expect(
            base64.stringify(new Uint8Array(deriveBits)) == base64.stringify(serverKey)
          ).toBeTrue();
          done();
        });
    });
  });

  it('check the Random-128, 256, 512', () => {
    expect(RandomKeyGenerator128.new_key().length == 16).toBeTrue();
    expect(RandomKeyGenerator256.new_key().length == 32).toBeTrue();
    expect(RandomKeyGenerator512.new_key().length == 64).toBeTrue();
  });
});
