/*
 * 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 mathematical = require('../lib/index');
var CommonTestData = require('./data/common-data');
var secureRandom = require('scytl-securerandom');
var forge = require('node-forge');

var BigInteger = forge.jsbn.BigInteger;

describe('The mathematical module should be able to ...', function() {
  var TEST_SEED = new Uint8Array([18, 58, 188]);

  var mathService_;
  var mathArrayCompressor_;
  var mathRandomGenerator_;
  var mathGroupHandler_;

  var g_;
  var p_;
  var q_;
  var group_;
  var anotherGroup_;
  var qrGroup_;
  var elementValue_;
  var element_;
  var exponentValue_;
  var exponent_;
  var element1Value_;
  var element2Value_;
  var element3Value_;
  var elements_;
  var exponents_;
  var qrGroupLarge_;
  var exponent1Large_;
  var exponent2Large_;
  var multiGroupElementValueArray_;
  var multiGroupElementValues_;
  var multiGroupElements_;
  var minCertaintyLevel_;

  beforeAll(function() {
    mathService_ = mathematical.newService();

    mathArrayCompressor_ = mathService_.newArrayCompressor();
    mathRandomGenerator_ = mathService_.newRandomGenerator();
    mathGroupHandler_ = mathService_.newGroupHandler();

    var testData = new CommonTestData();

    p_ = testData.getP();
    q_ = testData.getQ();
    g_ = testData.getG();
    group_ = testData.getGroup();
    anotherGroup_ = testData.getAnotherGroup();
    qrGroup_ = testData.getQuadraticResidueGroup();

    elementValue_ = testData.getElementValue();
    element_ = testData.getElement();

    exponentValue_ = testData.getExponentValue();
    exponent_ = testData.getExponent();

    element1Value_ = testData.getElement1Value();
    element2Value_ = testData.getElement2Value();
    element3Value_ = testData.getElement3Value();
    elements_ = testData.getElements();

    exponents_ = testData.getExponents();

    qrGroupLarge_ = testData.getLargeQuadraticResidueGroup();

    exponent1Large_ = testData.getExponent1Large();
    exponent2Large_ = testData.getExponent2Large();

    multiGroupElementValueArray_ = testData.getMultiGroupElementValueArray();
    multiGroupElementValues_ = testData.multiGroupElementValues();
    multiGroupElements_ = testData.getMultiGroupElements();

    minCertaintyLevel_ = testData.getMinimumCertaintyLevel();
  });

  describe('create a mathematical service that should be able to ..', function() {
    it('be created from a secure random service object provided as input',
       function() {
         var mathService = mathematical.newService(
             {secureRandomService: secureRandom.newService()});
         var mathRandomGenerator = mathService.newRandomGenerator();
         var randomExponent = mathRandomGenerator.nextExponent(qrGroup_);
         expect(randomExponent).toBeDefined();
       });

    describe(
        'create a new ZpSubgroup object that should be able to ..', function() {
          it('be defined', function() {
            expect(group_).toBeDefined();
            expect(group_.identity).toBeDefined();
          });

          it('check group membership', function() {
            var element =
                mathService_.newZpGroupElement(p_, q_, BigInteger.ONE);
            expect(group_.isGroupMember(element)).toBeTruthy();

            // Check membership of element with wrong q.
            var q2 = new BigInteger('19');
            var p2 = new BigInteger('37');
            var group2 = mathService_.newZpSubgroup(p2, q2, g_);
            var element2 = mathService_.newZpGroupElement(
                group2.p, group2.q, BigInteger.ONE);
            expect(!group_.isGroupMember(element2)).toBeTruthy();

            // Check membership of non-member element.
            element =
                mathService_.newZpGroupElement(p_, q_, new BigInteger('5'));
            expect(!group_.isGroupMember(element)).toBeTruthy();
          });

          it('be compared with another ZpSubgroup object', function() {
            var group2 = mathService_.newZpSubgroup(p_, q_, g_);

            var q2 = new BigInteger('19');
            var p2 = new BigInteger('37');
            var group3 = mathService_.newZpSubgroup(p2, q2, g_);

            expect(group_.equals(group2)).toBeTruthy();
            expect(!group_.equals(group3)).toBeTruthy();
          });

          it('be serialized and deserialized', function() {
            var groupJson = group_.toJson();

            var groupFromJson = mathService_.newZpSubgroup(groupJson);

            expect(groupFromJson).toBeDefined();
            expect(groupFromJson.equals(group_)).toBeTruthy();
          });
        });

    describe(
        'create a new ZpGroupElement object that should be able to ..',
        function() {
          it('be defined', function() {
            expect(element_).toBeDefined();
            expect(element_.value.equals(elementValue_)).toBeTruthy();
          });

          it('be multiplied with another ZpGroupElement object', function() {
            var value = new BigInteger('2');
            var element2 = mathService_.newZpGroupElement(p_, q_, value);
            var groupResult = element_.multiply(element2);

            expect(groupResult).toBeDefined();
            expect(groupResult.value.equals(new BigInteger('6'))).toBeTruthy();
          });

          it('be exponentiated with an Exponent object', function() {
            var value = new BigInteger('2');
            var exponent = mathService_.newExponent(q_, value);
            var groupResult = element_.exponentiate(exponent);

            expect(groupResult).toBeDefined();
            expect(groupResult.value.equals(new BigInteger('9'))).toBeTruthy();
          });

          it('be inverted', function() {
            var groupResult = element_.invert();

            expect(groupResult).toBeDefined();
            expect(groupResult.value.equals(new BigInteger('8'))).toBeTruthy();
          });

          it('be serialized and deserialized', function() {
            var elementJson = element_.toJson();

            var elementFromJson = mathService_.newZpGroupElement(elementJson);

            expect(elementFromJson).toBeDefined();
            expect(elementFromJson.equals(element_)).toBeTruthy();
          });
        });

    describe(
        'create a new Exponent object that should be able to ..', function() {
          it('be defined', function() {
            expect(exponent_).toBeDefined();
            expect(exponent_.value.equals(exponentValue_)).toBeTruthy();
          });

          it('be added to another Exponent object', function() {
            var value = new BigInteger('3');
            var exponent2 = mathService_.newExponent(q_, value);

            var exponentResult = exponent_.add(exponent2);
            expect(exponentResult).toBeDefined();
            expect(exponentResult.value.equals(new BigInteger('5')))
                .toBeTruthy();
          });

          it('subtract another Exponent object from itself', function() {
            var value = new BigInteger('5');
            var exponent2 = mathService_.newExponent(q_, value);

            var exponentResult = exponent2.subtract(exponent_);
            expect(exponentResult).toBeDefined();
            expect(exponentResult.value.equals(new BigInteger('3')))
                .toBeTruthy();
          });

          it('be multiplied with another Exponent object', function() {
            var value = new BigInteger('3');
            var exponent2 = mathService_.newExponent(q_, value);

            var exponentResult = exponent_.multiply(exponent2);
            expect(exponentResult).toBeDefined();
            expect(exponentResult.value.toString())
                .toEqual('' + exponent_.value * exponent2.value % q_);
          });

          it('be multiplied with Exponent objects encapsulating large values',
             function() {
               var exponentResult = exponent1Large_.multiply(exponent2Large_);
               expect(exponentResult).toBeDefined();
             });

          it('be negated', function() {
            var exponentResult = exponent_.negate();

            expect(exponentResult).toBeDefined();
            expect(exponentResult.value.equals(new BigInteger('9')))
                .toBeTruthy();
          });

          it('be serialized and deserialized', function() {
            var exponentJson = exponent_.toJson();

            var exponentFromJson = mathService_.newExponent(exponentJson);

            expect(exponentFromJson).toBeDefined();
            expect(exponentFromJson.equals(exponent_)).toBeTruthy();
          });
        });

    it('create a new quadratic residue Zp subgroup', function() {
      var group = mathService_.newQuadraticResidueGroup(p_, g_);
      expect(group).toBeDefined();
    });

    describe(
        'create a new MathematicalArrayCompressor object that should be able to ..',
        function() {
          it('compress Zp group elements', function() {
            var compressedElement =
                mathArrayCompressor_.compressZpGroupElements(elements_);

            expect(compressedElement).toBeDefined();
            expect(compressedElement.value.equals(new BigInteger('8')))
                .toBeTruthy();

            compressedElement = mathArrayCompressor_.compressZpGroupElements(
                multiGroupElements_, group_);
            expect(compressedElement).toBeDefined();
            expect(compressedElement.value.equals(new BigInteger('6')))
                .toBeTruthy();
          });

          it('compress a desired number of trailing Zp group elements',
             function() {
               var requiredLength = 4;

               var compressedArray =
                   mathArrayCompressor_.compressTrailingZpGroupElements(
                       multiGroupElements_, requiredLength);
               expect(compressedArray.length).toBe(requiredLength);

               var expectedCompressedArray = [];
               var expectedCompressedArrayValues = [3, 4, 5, 7];
               for (var i = 0; i < expectedCompressedArrayValues.length; i++) {
                 var value =
                     new BigInteger('' + expectedCompressedArrayValues[i]);
                 var element = mathService_.newZpGroupElement(p_, q_, value);
                 expectedCompressedArray.push(element);
               }

               var equals = true;
               for (var j = 0; j < compressedArray.length; j++) {
                 if (!compressedArray[j].equals(expectedCompressedArray[j])) {
                   equals = false;
                 }
               }
               expect(equals).toBeTruthy();
             });

          it('compress exponents', function() {
            var compressedExponent =
                mathArrayCompressor_.compressExponents(exponents_);

            expect(compressedExponent).toBeDefined();
            expect(compressedExponent.value.equals(new BigInteger('7')))
                .toBeTruthy();
          });

          it('compress a desired number of trailing exponents', function() {
            var requiredLength = 2;

            var compressedArray =
                mathArrayCompressor_.compressTrailingExponents(
                    exponents_, requiredLength);
            expect(compressedArray.length).toBe(requiredLength);

            var value;
            var exponent;
            var expectedCompressedArrayValues = [2, 5];
            var expectedCompressedArray = [];
            for (var i = 0; i < expectedCompressedArrayValues.length; i++) {
              value = new BigInteger('' + expectedCompressedArrayValues[i]);
              exponent = mathService_.newExponent(q_, value);
              expectedCompressedArray.push(exponent);
            }

            var equals = true;
            for (var j = 0; j < compressedArray.length; j++) {
              if (!compressedArray[j].equals(expectedCompressedArray[j])) {
                equals = false;
              }
            }
            expect(equals).toBeTruthy();
          });
        });

    describe(
        'create a new MathematicalRandomGenerator object that should be able to ..',
        function() {
          it('generate a random Exponent', function() {
            var randomExponent = mathRandomGenerator_.nextExponent(qrGroup_);
            expect(randomExponent).toBeDefined();
          });

          it('generate a short random Exponent', function() {
            var randomExponent = mathRandomGenerator_.nextExponent(
                qrGroupLarge_, {useShortExponent: true});
            expect(randomExponent).toBeDefined();

            expect(function() {
              mathRandomGenerator_.nextExponent(
                  qrGroup_, {useShortExponent: true});
            }).toThrow();
          });

          it('generate a random Exponent, using chosen source of randomness',
             function() {
               var secureRandomService =
                   secureRandom.newService({prngSeed: TEST_SEED});
               var randomGenerator = secureRandomService.newRandomGenerator();

               var randomExponent = mathRandomGenerator_.nextExponent(
                   qrGroup_, {secureRandomGenerator: randomGenerator});

               expect(randomExponent.value.toString()).toBe('9');
             });

          it('generate a random Zp group element', function() {
            for (var i = 0; i < 100; i++) {
              var element = mathRandomGenerator_.nextZpGroupElement(qrGroup_);
              expect(element).toBeDefined();
              expect(qrGroup_.isGroupMember(element)).toBeTruthy();
            }
          });

          it('generate a random Zp group element, using a short exponent',
             function() {
               var element = mathRandomGenerator_.nextZpGroupElement(
                   qrGroupLarge_, {useShortExponent: true});
               expect(element).toBeDefined();
               expect(qrGroupLarge_.isGroupMember(element)).toBeTruthy();
             });

          it('generate a random Zp group element, using a chosen source of randomness',
             function() {
               var secureRandomService =
                   secureRandom.newService({prngSeed: TEST_SEED});
               var randomGenerator = secureRandomService.newRandomGenerator();

               var randomElement = mathRandomGenerator_.nextZpGroupElement(
                   qrGroup_, {secureRandomGenerator: randomGenerator});

               expect(randomElement.value.toString()).toBe('6');
             });

          it('generate a random quadratic residue Zp subgroup', function() {
            var bitLengthOfP = 16;
            var certainty = minCertaintyLevel_;

            var group = mathRandomGenerator_.nextQuadraticResidueGroup(
                bitLengthOfP, certainty);

            expect(group).toBeDefined();
          });

          it('generate a random quadratic residue Zp subgroup, using a chosen source of randomness',
             function() {
               var bitLengthOfP = 16;
               var certainty = minCertaintyLevel_;
               var secureRandomService =
                   secureRandom.newService({prngSeed: TEST_SEED});
               var randomGenerator = secureRandomService.newRandomGenerator();

               var group = mathRandomGenerator_.nextQuadraticResidueGroup(
                   bitLengthOfP, certainty,
                   {secureRandomGenerator: randomGenerator});

               expect(group.p.toString()).toBe('41843');
               expect(group.q.toString()).toBe('20921');
               expect(group.generator.value.toString()).toBe('3');
             });
        });

    describe(
        'create a new MathematicalGroupHandler object that should be able to ..',
        function() {
          it('exponentiate an array of Zp group elements', function() {
            var exponentiatedElements = mathGroupHandler_.exponentiateElements(
                group_, elements_, exponent_);

            expect(exponentiatedElements).toBeDefined();
            expect(exponentiatedElements.length).toEqual(elements_.length);
            expect(exponentiatedElements[0].value.toString())
                .toEqual('' + (Math.pow(element1Value_, exponentValue_) % p_));
            expect(exponentiatedElements[1].value.toString())
                .toEqual('' + (Math.pow(element2Value_, exponentValue_) % p_));
            expect(exponentiatedElements[2].value.toString())
                .toEqual('' + (Math.pow(element3Value_, exponentValue_) % p_));

            expect(exponentiatedElements[0].p).toEqual(p_);
            expect(exponentiatedElements[1].p).toEqual(p_);
            expect(exponentiatedElements[2].p).toEqual(p_);

            expect(exponentiatedElements[0].q).toEqual(q_);
            expect(exponentiatedElements[1].q).toEqual(q_);
            expect(exponentiatedElements[2].q).toEqual(q_);
          });

          it('honor the validation flag when exponentiating an array of Zp group elements',
             function() {
               // Confirm that no error is thrown when exponentiating some
               // elements that do not belong to group but validation flag is
               // not set.
               mathGroupHandler_.exponentiateElements(
                   group_, multiGroupElements_, exponent_);

               // Confirm that no error is thrown when exponentiating some
               // elements that do not belong to group but validation flag is
               // set to false.
               mathGroupHandler_.exponentiateElements(
                   group_, multiGroupElements_, exponent_, false);

               // Confirm that error is thrown when exponentiating some elements
               // that do not belong to group and validation flag is set to
               // true.
               expect(function() {
                 mathGroupHandler_.exponentiateElements(
                     group_, multiGroupElements_, exponent_, true);
               }).toThrow();
             });

          it('divide two Zp group element arrays', function() {
            var value;
            var element;
            var elements = [];

            value = new BigInteger('1');
            element = mathService_.newZpGroupElement(p_, q_, value);
            elements.push(element);

            value = new BigInteger('2');
            element = mathService_.newZpGroupElement(p_, q_, value);
            elements.push(element);

            value = new BigInteger('16');
            element = mathService_.newZpGroupElement(p_, q_, value);
            elements.push(element);

            var dividedElements =
                mathGroupHandler_.divideElements(elements_, elements, group_);

            expect(dividedElements).toBeDefined();
          });

          it('check the Zp subgroup membership of an array of Zp group elements, for which all elements are members',
             function() {
               mathGroupHandler_.checkGroupMembership(group_, elements_);
             });

          it('check the Zp subgroup membership of an array of Zp group elements, for which not all elements are members',
             function() {
               expect(function() {
                 mathGroupHandler_.checkGroupMembership(
                     anotherGroup_, elements_);
               }).toThrow();
             });

          it('extract a specified number of Zp subgroup members from an array of Zp group element values',
             function() {
               var numMembersRequired = multiGroupElementValueArray_.length - 2;

               var extractedElements = mathGroupHandler_.extractGroupMembers(
                   group_, multiGroupElementValues_, numMembersRequired);
               expect(extractedElements).toBeDefined();
               expect(extractedElements.length).toBe(numMembersRequired);
             });
        });
  });
});
