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 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();

          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();
  });
});
