/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
/* jshint node:true */
'use strict';

var CommonTestData = require('./data/common-data');
var elGamal = require('../lib/index');
var mathematical = require('scytl-mathematical');
var secureRandom = require('scytl-securerandom');

describe('The ElGamal cryptography module should be able to ...', function() {
  var elGamalService_;
  var secureRandomService_;
  var mathService_;
  var mathRandomGenerator_;
  var group_;
  var publicKey_;
  var privateKey_;
  var publicKeyElements_;
  var privateKeyExponents_;
  var keyPair_;
  var elements_;
  var elementValueStrings_;
  var largePublicKey_;
  var largePrivateKey_;
  var elementsFromLargeGroup_;
  var elementsFromLargeNonQrGroup_;
  var identityElements_;
  var shortExponentBitLength_;
  var singleElements_;
  var elGamalEncrypter_;
  var elGamalDecrypter_;
  var encryptedElements_;
  var elGamalEncrypterLargeGroup_;
  var elGamalDecrypterLargeGroup_;
  var prngSeed_;
  var encrypterRandomGenerator_;
  var decrypterRandomGenerator_;

  beforeAll(function() {
    elGamalService_ = elGamal.newService();
    secureRandomService_ = secureRandom.newService();
    mathService_ =
        mathematical.newService({secureRandomService: secureRandomService_});
    mathRandomGenerator_ = mathService_.newRandomGenerator();

    var testData = new CommonTestData();
    group_ = testData.getGroup();
    publicKey_ = testData.getPublicKey();
    privateKey_ = testData.getPrivateKey();
    publicKeyElements_ = testData.getPublicKeyElements();
    privateKeyExponents_ = testData.getPrivateKeyExponents();
    keyPair_ = testData.getKeyPair();
    elements_ = testData.getZpGroupElements();
    elementValueStrings_ = testData.getZpGroupElementValueStrings();
    largePublicKey_ = testData.getLargePublicKey();
    largePrivateKey_ = testData.getLargePrivateKey();
    elementsFromLargeGroup_ = testData.getElementsFromLargeZpSubgroup();
    elementsFromLargeNonQrGroup_ =
        testData.getElementsFromLargeNonQrZpSubgroup();
    identityElements_ = testData.getIdentityElements();
    singleElements_ = testData.getSingleElements();
    shortExponentBitLength_ = testData.getShortExponentBitLength();

    elGamalEncrypter_ = elGamalService_.newEncrypter().init(publicKey_);
    elGamalDecrypter_ = elGamalService_.newDecrypter().init(privateKey_);

    encryptedElements_ = elGamalEncrypter_.encrypt(elements_);

    elGamalEncrypterLargeGroup_ =
        elGamalService_.newEncrypter().init(largePublicKey_);
    elGamalDecrypterLargeGroup_ =
        elGamalService_.newDecrypter().init(largePrivateKey_);

    prngSeed_ = secureRandomService_.nextSeed();
  });

  beforeEach(function() {
    encrypterRandomGenerator_ =
        secureRandom.newService({prngSeed: prngSeed_}).newRandomGenerator();

    decrypterRandomGenerator_ =
        secureRandom.newService({prngSeed: prngSeed_}).newRandomGenerator();
  });

  describe('create an ElGamal cryptography service that should be able to ..', function() {
    it('create a new ElGamal key pair', function() {
      validatePublicKey(keyPair_.publicKey, group_);
      validatePrivateKey(keyPair_.privateKey, group_);
    });

    it('create a new ElGamalEncryptedElements object', function() {
      var encryptedElements = elGamalEncrypter_.encrypt(elements_);

      var gamma = encryptedElements.gamma;
      var phis = encryptedElements.phis;

      var newEncryptedElements =
          elGamalService_.newEncryptedElements(gamma, phis);

      var decryptedElements = elGamalDecrypter_.decrypt(newEncryptedElements);

      checkDecryptedElements(decryptedElements, elements_);
    });

    it('create a new ElGamalEncryptedElements object, with a saved secret',
       function() {
         var encryptedElementsAndSecret =
             elGamalEncrypter_.encrypt(elements_, {saveSecret: true});

         var gamma = encryptedElementsAndSecret.gamma;
         var phis = encryptedElementsAndSecret.phis;
         var secret = encryptedElementsAndSecret.secret;

         var newEncryptedElementsAndSecret =
             elGamalService_.newEncryptedElements(gamma, phis, secret);

         expect((newEncryptedElementsAndSecret.secret.equals(secret)))
             .toBeTruthy();

         var encryptedElements = elGamalService_.newEncryptedElements(
             newEncryptedElementsAndSecret.gamma,
             newEncryptedElementsAndSecret.phis);

         var decryptedElements = elGamalDecrypter_.decrypt(encryptedElements);
         checkDecryptedElements(decryptedElements, elements_);
       });

    it('serialize and deserialize an ElGamal public key', function() {
      var publicKeyJson = publicKey_.toJson();

      var publicKeyFromJson = elGamalService_.newPublicKey(publicKeyJson);

      validatePublicKey(publicKeyFromJson, group_);
    });

    it('serialize and deserialize an ElGamal private key', function() {
      var privateKeyJson = privateKey_.toJson();

      var privateKeyFromJson = elGamalService_.newPrivateKey(privateKeyJson);

      validatePrivateKey(privateKeyFromJson, group_);
    });

    it('serialize and deserialize ElGamal encrypted elements', function() {
      var encryptedElements = elGamalEncrypter_.encrypt(elements_);

      var encryptedElementsJson = encryptedElements.toJson();
      var encryptedElementsFromJson =
          elGamalService_.newEncryptedElements(encryptedElementsJson);

      var decryptedElements =
          elGamalDecrypter_.decrypt(encryptedElementsFromJson);

      checkDecryptedElements(decryptedElements, elements_);
    });

    it('serialize and deserialize an encryption pre-computation', function() {
      var preComputation = elGamalEncrypter_.preCompute();

      var preComputationJson = preComputation.toJson();
      var preComputationFromJson =
          elGamalService_.newEncryptedElements(preComputationJson);

      var encryptedElements = elGamalEncrypter_.encrypt(
          elements_, {preComputation: preComputationFromJson});

      var decryptedElements = elGamalDecrypter_.decrypt(encryptedElements);

      checkDecryptedElements(decryptedElements, elements_);
    });

    describe('create an encrypter/decrypter pair that should be able to ..', function() {
      it('encrypt and decrypt some group elements', function() {
        var decryptedElements = elGamalDecrypter_.decrypt(encryptedElements_);

        checkDecryptedElements(decryptedElements, elements_);
      });

      it('encrypt and decrypt some group elements, using a specified secure random service object',
         function() {
           var elGamalService =
               elGamal.newService({secureRandomService: secureRandomService_});
           var elGamalEncrypter =
               elGamalService.newEncrypter().init(publicKey_);
           var elGamalDecrypter =
               elGamalService.newDecrypter().init(privateKey_);

           var encryptedElements = elGamalEncrypter.encrypt(elements_);
           var decryptedElements = elGamalDecrypter.decrypt(encryptedElements);

           checkDecryptedElements(decryptedElements, elements_);
         });

      it('encrypt and decrypt some group elements, using a specified mathematical service object',
         function() {
           var elGamalService =
               elGamal.newService({mathematicalService: mathService_});
           var elGamalEncrypter =
               elGamalService.newEncrypter().init(publicKey_);
           var elGamalDecrypter =
               elGamalService.newDecrypter().init(privateKey_);

           var encryptedElements = elGamalEncrypter.encrypt(elements_);
           var decryptedElements = elGamalDecrypter.decrypt(encryptedElements);

           checkDecryptedElements(decryptedElements, elements_);
         });

      it('encrypt and decrypt some group element value strings', function() {
        var encryptedElements = elGamalEncrypter_.encrypt(elementValueStrings_);

        var decryptedElements = elGamalDecrypter_.decrypt(encryptedElements);

        checkDecryptedElements(decryptedElements, elements_);
      });

      it('decrypt some group elements, with the check group membership flag set to true',
         function() {
           var decryptedElements = elGamalDecrypter_.decrypt(
               encryptedElements_, {checkMembership: true});

           checkDecryptedElements(decryptedElements, elements_);
         });

      it('decrypt some group elements, with check group membership flag set to false',
         function() {
           var decryptedElements = elGamalDecrypter_.decrypt(
               encryptedElements_, {checkMembership: false});

           checkDecryptedElements(decryptedElements, elements_);
         });

      it('throw an error when decrypting some group elements, the check group membership flag is set to true and the check fails',
         function() {
           expect(function() {
             elGamalDecrypterLargeGroup_.decrypt(
                 encryptedElements_, {checkMembership: true});
           }).toThrow();
         });

      it('encrypt and decrypt some group elements, using an encryption pre-computation',
         function() {
           var preComputation = elGamalEncrypter_.preCompute();
           var encryptedElements = elGamalEncrypter_.encrypt(
               elements_, {preComputation: preComputation});

           var decryptedElements = elGamalDecrypter_.decrypt(encryptedElements);

           checkDecryptedElements(decryptedElements, elements_);
         });

      it('encrypt and decrypt some group elements and retrieve the secret exponent',
         function() {
           var encryptedElementsAndSecret =
               elGamalEncrypter_.encrypt(elements_, {saveSecret: true});

           checkSecretExponent(
               publicKey_, encryptedElementsAndSecret, elements_);

           var encryptedElements = elGamalService_.newEncryptedElements(
               encryptedElementsAndSecret.gamma,
               encryptedElementsAndSecret.phis);

           var decryptedElements = elGamalDecrypter_.decrypt(encryptedElements);

           checkDecryptedElements(decryptedElements, elements_);
         });

      it('encrypt and decrypt some group elements, using an encryption pre-computation, and retrieve the secret exponent after pre-computing',
         function() {
           var preComputationAndSecret =
               elGamalEncrypter_.preCompute({saveSecret: true});

           checkSecretExponent(
               publicKey_, preComputationAndSecret, identityElements_);

           var preComputation = elGamalService_.newEncryptedElements(
               preComputationAndSecret.gamma, preComputationAndSecret.phis);

           var encryptedElements = elGamalEncrypter_.encrypt(
               elements_, {preComputation: preComputation});

           var decryptedElements = elGamalDecrypter_.decrypt(encryptedElements);

           checkDecryptedElements(decryptedElements, elements_);
         });

      it('encrypt and decrypt some group elements, using encrypted elements generated with a short exponent',
         function() {
           var encryptedElementsAndSecret = elGamalEncrypterLargeGroup_.encrypt(
               elementsFromLargeGroup_,
               {useShortExponent: true, saveSecret: true});
           expect(encryptedElementsAndSecret.secret.value.bitLength())
               .not.toBeGreaterThan(shortExponentBitLength_);

           var encryptedElements = elGamalService_.newEncryptedElements(
               encryptedElementsAndSecret.gamma,
               encryptedElementsAndSecret.phis);

           var decryptedElements =
               elGamalDecrypterLargeGroup_.decrypt(encryptedElements);

           checkDecryptedElements(decryptedElements, elementsFromLargeGroup_);
         });

      it('encrypt and decrypt some group elements, using an encryption pre-computation generated with a short exponent',
         function() {
           var preComputationAndSecret = elGamalEncrypterLargeGroup_.preCompute(
               {useShortExponent: true, saveSecret: true});
           expect(preComputationAndSecret.secret.value.bitLength())
               .not.toBeGreaterThan(shortExponentBitLength_);

           var preComputation = elGamalService_.newEncryptedElements(
               preComputationAndSecret.gamma, preComputationAndSecret.phis);

           var encryptedElements = elGamalEncrypterLargeGroup_.encrypt(
               elementsFromLargeGroup_, {preComputation: preComputation});

           var decryptedElements =
               elGamalDecrypterLargeGroup_.decrypt(encryptedElements);

           checkDecryptedElements(decryptedElements, elementsFromLargeGroup_);
         });

      it('encrypt and decrypt some group elements, when the public key and private key undergo compression',
         function() {
           var encryptedElements = elGamalEncrypter_.encrypt([elements_[0]]);

           var decryptedElements = elGamalDecrypter_.decrypt(encryptedElements);

           checkDecryptedElements(decryptedElements, [elements_[0]]);
         });

      it('throw an error when encrypting before the encrypter has been initialized with a public key',
         function() {
           var encrypter = elGamalService_.newEncrypter();

           expect(function() {
             encrypter.encrypt(elements_);
           }).toThrow();
         });

      it('throw an error when decrypting before the decrypter has been initialized with a private key',
         function() {
           var decrypter = elGamalService_.newDecrypter();

           expect(function() {
             decrypter.decrypt(encryptedElements_);
           }).toThrow();
         });

      it('throw an error when decrypting before the PRNG decrypter has been initialized with a public key',
         function() {
           var prngDecrypter = elGamalService_.newPrngDecrypter();

           expect(function() {
             prngDecrypter.decrypt(encryptedElements_);
           }).toThrow();
         });

      it('throw an error when attempting to decrypt non-group members and group membership is being checked',
         function() {
           expect(function() {
             var encryptedElements =
                 elGamalEncrypterLargeGroup_.encrypt(elementsFromLargeGroup_);

             elGamalDecrypter_.decrypt(
                 encryptedElements, {checkMembership: true});
           }).toThrow();
         });

      it('throw an error when attempting to ElGamal encrypt, using a short exponent, for a group that is not of type quadratic residue',
         function() {
           expect(function() {
             elGamalEncrypterLargeGroup_.encrypt(
                 elementsFromLargeNonQrGroup_, {useShortExponent: true});
           }).toThrow();
         });

      it('throw an error when attempting to ElGamal encrypt, using a short exponent, for a group whose order is smaller than this exponent',
         function() {
           expect(function() {
             elGamalEncrypter_.encrypt(elements_, {useShortExponent: true});
           }).toThrow();
         });
    });

    describe(
        'create an encrypter/PRNG decrypter pair that should be able to ..',
        function() {
          it('encrypt and decrypt some group elements', function() {
            var encrypterFromSeed = elGamalService_.newEncrypter().init(
                publicKey_, {secureRandomGenerator: encrypterRandomGenerator_});
            var encryptedElements = encrypterFromSeed.encrypt(elements_);

            var prngDecrypter = elGamalService_.newPrngDecrypter().init(
                publicKey_, decrypterRandomGenerator_);
            var decryptedElements = prngDecrypter.decrypt(encryptedElements);

            checkDecryptedElements(decryptedElements, elements_);
          });

          it('encrypt and decrypt some group elements, using an encryption pre-computation',
             function() {
               var encrypterFromSeed = elGamalService_.newEncrypter().init(
                   publicKey_,
                   {secureRandomGenerator: encrypterRandomGenerator_});
               var preComputation = encrypterFromSeed.preCompute();
               var encryptedElements = encrypterFromSeed.encrypt(
                   elements_, {preComputation: preComputation});

               var prngDecrypter = elGamalService_.newPrngDecrypter().init(
                   publicKey_, decrypterRandomGenerator_);
               var decryptedElements = prngDecrypter.decrypt(encryptedElements);

               checkDecryptedElements(decryptedElements, elements_);
             });

          it('encrypt some group elements and retrieve the secret exponent that will be used for decryption',
             function() {
               var encrypterFromSeed = elGamalService_.newEncrypter().init(
                   publicKey_,
                   {secureRandomGenerator: encrypterRandomGenerator_});
               var encryptedElementsAndSecret =
                   encrypterFromSeed.encrypt(elements_, {saveSecret: true});

               var secret = encryptedElementsAndSecret.secret;
               var secretFromDecrypter = mathRandomGenerator_.nextExponent(
                   group_, {secureRandomGenerator: decrypterRandomGenerator_});
               expect(secretFromDecrypter.equals(secret)).toBeTruthy();
             });

          it('encrypt some group elements, using pre-computation, and retrieve the secret exponent that will be used for decryption',
             function() {
               var encrypterFromSeed = elGamalService_.newEncrypter().init(
                   publicKey_,
                   {secureRandomGenerator: encrypterRandomGenerator_});
               var preComputationAndSecret =
                   encrypterFromSeed.preCompute({saveSecret: true});

               var secret = preComputationAndSecret.secret;
               var secretFromDecrypter = mathRandomGenerator_.nextExponent(
                   group_, {secureRandomGenerator: decrypterRandomGenerator_});
               expect(secretFromDecrypter.equals(secret)).toBeTruthy();
             });

          it('encrypt and decrypt some group elements, using encrypted elements generated with a short exponent',
             function() {
               var encrypterFromSeed = elGamalService_.newEncrypter().init(
                   largePublicKey_,
                   {secureRandomGenerator: encrypterRandomGenerator_});
               var encryptedElementsAndSecret = encrypterFromSeed.encrypt(
                   elementsFromLargeGroup_,
                   {useShortExponent: true, saveSecret: true});
               expect(encryptedElementsAndSecret.secret.value.bitLength())
                   .not.toBeGreaterThan(shortExponentBitLength_);

               var encryptedElements = elGamalService_.newEncryptedElements(
                   encryptedElementsAndSecret.gamma,
                   encryptedElementsAndSecret.phis);

               var prngDecrypter = elGamalService_.newPrngDecrypter().init(
                   largePublicKey_, decrypterRandomGenerator_);
               var decryptedElements = prngDecrypter.decrypt(
                   encryptedElements, {useShortExponent: true});

               checkDecryptedElements(
                   decryptedElements, elementsFromLargeGroup_);
             });

          it('encrypt and decrypt some group elements, using an encryption pre-computation generated with a short exponent',
             function() {
               var encrypterFromSeed = elGamalService_.newEncrypter().init(
                   largePublicKey_,
                   {secureRandomGenerator: encrypterRandomGenerator_});
               var preComputationAndSecret = encrypterFromSeed.preCompute(
                   {useShortExponent: true, saveSecret: true});
               expect(preComputationAndSecret.secret.value.bitLength())
                   .not.toBeGreaterThan(shortExponentBitLength_);
               var preComputation = elGamalService_.newEncryptedElements(
                   preComputationAndSecret.gamma, preComputationAndSecret.phis);
               var encryptedElements = encrypterFromSeed.encrypt(
                   elementsFromLargeGroup_, {preComputation: preComputation});

               var prngDecrypter = elGamalService_.newPrngDecrypter().init(
                   largePublicKey_, decrypterRandomGenerator_);
               var decryptedElements = prngDecrypter.decrypt(
                   encryptedElements, {useShortExponent: true});

               checkDecryptedElements(
                   decryptedElements, elementsFromLargeGroup_);
             });

          it('encrypt and decrypt an array of single group elements',
             function() {
               var encrypterFromSeed = elGamalService_.newEncrypter().init(
                   largePublicKey_,
                   {secureRandomGenerator: encrypterRandomGenerator_});
               var encryptedElementArray = [];
               for (var i = 0; i < singleElements_.length; i++) {
                 encryptedElementArray.push(
                     encrypterFromSeed.encrypt([singleElements_[i]]));
               }

               var prngDecrypter = elGamalService_.newPrngDecrypter().init(
                   largePublicKey_, decrypterRandomGenerator_);
               var decryptedElementArray = [];
               for (var j = 0; j < encryptedElementArray.length; j++) {
                 decryptedElementArray.push(
                     prngDecrypter.decrypt(encryptedElementArray[j])[0]);
               }

               checkDecryptedElements(decryptedElementArray, singleElements_);
             });
        });
  });

  function validatePublicKey(publicKey, group) {
    var elements = publicKey.elements;
    var numElements = elements.length;
    expect(numElements).toBe(publicKeyElements_.length);
    for (var i = 0; i < numElements; i++) {
      expect(elements[i].value.toString())
          .toEqual(publicKeyElements_[i].value.toString());
    }

    expect(publicKey.group.equals(group)).toBeTruthy();
  }

  function validatePrivateKey(privateKey, group) {
    var exponents = privateKey.exponents;
    var numExponents = exponents.length;
    expect(numExponents).toBe(privateKeyExponents_.length);
    for (var i = 0; i < numExponents; i++) {
      expect(exponents[i].value.toString())
          .toEqual(privateKeyExponents_[i].value.toString());
    }

    expect(privateKey.group.equals(group)).toBeTruthy();
  }

  function checkDecryptedElements(decryptedElements, elements) {
    expect(decryptedElements.length).toBe(elements.length);

    for (var i = 0; i < decryptedElements.length; i++) {
      expect(decryptedElements[i].equals(elements[i])).toBeTruthy();
    }
  }

  function checkSecretExponent(
      publicKey, encryptedElementsAndSecret, elements) {
    var publicKeyElements = publicKey.elements;

    var gamma = encryptedElementsAndSecret.gamma;
    var phis = encryptedElementsAndSecret.phis;
    var secret = encryptedElementsAndSecret.secret;

    expect((group_.generator.exponentiate(secret)).equals(gamma)).toBeTruthy();
    for (var i = 0; i < phis.length; i++) {
      expect((publicKeyElements[i].exponentiate(secret).multiply(elements[i]))
                 .equals(phis[i]))
          .toBeTruthy();
    }
  }
});
