/*
 * 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
 */
testUtils = (function() {

  'use strict';

  return {

    /**
     * Converts an Epoch time string to a Date object.
     *
     * @param {String}
     *            epochTimeStr the Epoch time string to convert.
     * @return {Date} the Date object.
     */
    epochTimeStringToDate: function(epochTimeStr) {

      return new Date(parseInt(epochTimeStr) * 1000);
    },

    /**
     * Serializes an error so that its information will be properly
     * displayed by the Java application that called the method that threw
     * the error.
     *
     * @param {String}
     *            error the error to serialize.
     * @param {String}
     *            data any data associated with the error.
     * @returns {String} the serialized error.
     */
    serializeError: function(error, data) {

      var details = {};

      function callback(key, value) {
        if (typeof value === 'object' && value !== null) {
          if (typeof details[key] !== 'undefined') {
            return;
          }
          details[key] = value;
        }
        return value;
      }

      var errorJson;
      if (typeof data !== 'undefined') {
        errorJson = JSON.stringify(
            {
              error: error.toString(),
              stack: error.stack,
              details: details,
              data: data
            },
            callback, 2);
      } else {
        errorJson = JSON.stringify(
            {error: error.toString(), stack: error.stack, details: details},
            callback, 2);
      }

      return errorJson.escapeNewlineChars().escapeDoubleQuoteChars();
    },

    /**
     * Checks a RSA private key against a reference RSA private key.
     *
     * @param {String}
     *            privateKeyPem the private key to check, in PEM format.
     * @param {String}
     *            privateKeyRefPem the reference private key, in PEM format.
     * @throws {Error}
     *             if the check fails.
     */
    checkPrivateKey: function(privateKeyPem, privateKeyRefPem) {

      var privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
      var privateKeyRef = forge.pki.privateKeyFromPem(privateKeyRefPem);

      var pFound = privateKey.p;
      var pExpected = privateKeyRef.p;
      if (!pFound.equals(pExpected)) {
        throw 'Expected p parameter: ' + pExpected + ' for private key ' +
            privateKeyPem + ' ; Found p parameter: ' + pFound;
      }

      var qFound = privateKey.q;
      var qExpected = privateKeyRef.q;
      if (!pFound.equals(pExpected)) {
        throw 'Expected q parameter: ' + qExpected + ' for private key ' +
            privateKeyPem + ' ; Found q parameter: ' + qFound;
      }
    },

    /**
     * Checks an RSA public key against a reference RSA public key.
     *
     * @param {String}
     *            publicKeyPem the public key to check, in PEM format.
     * @param {String}
     *            publicKeyRefPem the reference public key, in PEM format.
     * @throws {Error}
     *             if the check fails.
     */
    checkPublicKey: function(publicKeyPem, publicKeyRefPem) {

      var publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
      var publicKeyRef = forge.pki.publicKeyFromPem(publicKeyRefPem);

      var exponentFound = publicKey.e;
      var exponentExpected = publicKeyRef.e;
      if (!exponentFound.equals(exponentExpected)) {
        throw 'Expected exponent: ' + exponentExpected + ' for public key ' +
            publicKeyPem + ' ; Found exponent: ' + exponentFound;
      }

      var modulusFound = publicKey.n;
      var modulusExpected = publicKeyRef.n;
      if (!modulusFound.equals(modulusExpected)) {
        throw 'Expected modulus: ' + modulusExpected + ' for public key ' +
            publicKeyPem + ' ; Found modulus: ' + modulusFound;
      }
    },

    /**
     * Checks an X509 certificate against a reference X509 certificate.
     *
     * @param {String}
     *            certificatePem the certificate to check, in PEM format.
     * @param {String}
     *            certificateRefPem the reference certificate, in PEM
     *            format.
     * @throws {Error}
     *             if the check fails.
     */
    checkCertificate: function(certificatePem, certificateRefPem) {

      cryptolib('certificates.service', function(box) {
        var certificate = new box.certificates.service.load(certificatePem);
        var certificateRef =
            new box.certificates.service.load(certificateRefPem);

        var issuerDnFieldsRef = [
          certificateRef.getIssuerCN(), certificateRef.getIssuerCountry(),
          certificateRef.getIssuerOrgUnit(), certificateRef.getIssuerOrg()
        ];
        testUtils.checkCertificateIssuerDn(certificate, issuerDnFieldsRef);

        var subjectDnFieldsRef = [
          certificateRef.getSubjectCN(), certificateRef.getSubjectCountry(),
          certificateRef.getSubjectOrgUnit(), certificateRef.getSubjectOrg()
        ];
        testUtils.checkCertificateSubjectDn(certificate, subjectDnFieldsRef);

        var notBeforeRef =
            certificate.getNotBefore().getTime().toString().substring(
                0, box.EPOCH_TIME_LENGTH);
        var notAfterRef =
            certificate.getNotAfter().getTime().toString().substring(
                0, box.EPOCH_TIME_LENGTH);
        testUtils.checkCertificateValidityDates(
            certificate, notBeforeRef, notAfterRef);
      });
    },

    /**
     * Checks that a certificate contains the expected fields in its issuer
     * distinguished name.
     *
     * @param {Object}
     *            certificate the certificate to check, as a certificate
     *            object.
     * @param {Array}
     *            issuerDnFields the issuer DN fields, as an array of
     *            Strings.
     * @throws {Error}
     *             if an unexpected issuer distinguished name field is
     *             found.
     */
    checkCertificateIssuerDn: function(certificate, issuerDnFields) {

      var issuerCnFound = certificate.getIssuerCN();
      var issuerCountryFound = certificate.getIssuerCountry();
      var issuerOrgUnitFound = certificate.getIssuerOrgUnit();
      var issuerOrgFound = certificate.getIssuerOrg();

      var issuerCnExpected = issuerDnFields[0];
      var issuerCountryExpected = issuerDnFields[1];
      var issuerOrgUnitExpected = issuerDnFields[2];
      var issuerOrgExpected = issuerDnFields[3];

      if (issuerCnFound !== issuerCnExpected) {
        throw 'Expected issuer CN: ' + issuerCnExpected +
            ' ; Found: ' + issuerCnFound;
      }
      if (issuerCountryFound !== issuerCountryExpected) {
        throw 'Expected issuer country: ' + issuerCountryExpected +
            ' ; Found: ' + issuerCountryFound;
      }
      if (issuerOrgUnitFound !== issuerOrgUnitExpected) {
        throw 'Expected issuer organizational unit: ' + issuerOrgUnitExpected +
            ' ; Found: ' + issuerOrgUnitFound;
      }
      if (issuerOrgFound !== issuerOrgExpected) {
        throw 'Expected issuer organization: ' + issuerOrgExpected +
            ' ; Found: ' + issuerOrgFound;
      }
    },

    /**
     * Checks that a certificate contains the expected fields in its subject
     * distinguished name.
     *
     * @param {Object}
     *            certificate the certificate to check, as a certificate
     *            object.
     * @param {Array}
     *            subjectDnFields the subject DN fields, as an array of
     *            Strings.
     * @throws {Error}
     *             if an unexpected subject distinguished name field is
     *             found.
     */
    checkCertificateSubjectDn: function(certificate, subjectDnFields) {

      var subjectCnFound = certificate.getSubjectCN();
      var subjectCountryFound = certificate.getSubjectCountry();
      var subjectOrgUnitFound = certificate.getSubjectOrgUnit();
      var subjectOrgFound = certificate.getSubjectOrg();

      var subjectCnExpected = subjectDnFields[0];
      var subjectCountryExpected = subjectDnFields[1];
      var subjectOrgUnitExpected = subjectDnFields[2];
      var subjectOrgExpected = subjectDnFields[3];

      if (subjectCnFound !== subjectCnExpected) {
        throw 'Expected subject CN: ' + subjectCnExpected +
            ' ; Found: ' + subjectCnFound;
      }
      if (subjectCountryFound !== subjectCountryExpected) {
        throw 'Expected subject country: ' + subjectCountryExpected +
            ' ; Found: ' + subjectCountryFound;
      }
      if (subjectOrgUnitFound !== subjectOrgUnitExpected) {
        throw 'Expected subject organizational unit: ' +
            subjectOrgUnitExpected + ' ; Found: ' + subjectOrgUnitFound;
      }
      if (subjectOrgFound !== subjectOrgExpected) {
        throw 'Expected subject organization: ' + subjectOrgExpected +
            ' ; Found: ' + subjectOrgFound;
      }
    },

    /**
     * Checks that a certificate contains the expected validity dates.
     *
     * @param {Object}
     *            certificate the certificate to check, as a certificate
     *            object.
     * @param {String}
     *            notBefore the starting date of validity, as a String in
     *            Epoch time format.
     * @param {String}
     *            notAfter the ending date of validity, as a String in Epoch
     *            time format.
     * @throws {Error}
     *             if an unexpected validity date is found.
     */
    checkCertificateValidityDates: function(certificate, notBefore, notAfter) {

      cryptolib(function(box) {
        var notBeforeFound =
            certificate.getNotBefore().getTime().toString().substring(
                0, box.EPOCH_TIME_LENGTH);
        var notAfterFound =
            certificate.getNotAfter().getTime().toString().substring(
                0, box.EPOCH_TIME_LENGTH);

        if (notBeforeFound !== notBefore) {
          throw 'Expected starting validity date: ' + notBefore +
              ' ; Found: ' + notBeforeFound;
        }
        if (notAfterFound !== notAfter) {
          throw 'Expected ending validity date: ' + notAfter +
              ' ; Found: ' + notAfterFound;
        }
      });
    },

    /**
     * Checks a ElGamal private key against a reference ElGamal private key.
     *
     * @param {Object}
     *            elGamalPrivateKey the ElGamal private key to check.
     * @param {Object}
     *            elGamalPrivateKeyRef the reference ElGamal private key.
     * @throws {Error}
     *             if the check fails.
     */
    checkElGamalPrivateKey: function(elGamalPrivateKey, elGamalPrivateKeyRef) {

      var zpSubgroup = elGamalPrivateKey.getGroup();
      var zpSubgroupRef = elGamalPrivateKeyRef.getGroup();
      testUtils.checkZpSubgroup(zpSubgroup, zpSubgroupRef);

      var exponentsArray = elGamalPrivateKey.getExponentsArray();
      var exponentsArrayRef = elGamalPrivateKeyRef.getExponentsArray();
      testUtils.checkExponentsArray(exponentsArray, exponentsArrayRef);
    },

    /**
     * Checks a Zp subgroup against a reference Zp subgroup.
     *
     * @param {Object}
     *            zpSubgroup the Zp subgroup to check.
     * @param {Object}
     *            zpSubgroupRef the reference Zp subgroup.
     * @throws {Error}
     *             if the check fails.
     */
    checkZpSubgroup: function(zpSubgroup, zpSubgroupRef) {

      var pFound = zpSubgroup.getP().toString();
      var pExpected = zpSubgroupRef.getP().toString();
      if (!pFound.equals(pExpected)) {
        throw 'Expected p parameter: ' + pExpected +
            ' for Zp subgroup ; Found: ' + pFound;
      }

      var qFound = zpSubgroup.getQ().toString();
      var qExpected = zpSubgroupRef.getQ().toString();
      if (!qFound.equals(qExpected)) {
        throw 'Expected q parameter: ' + qExpected +
            ' for Zp subgroup ; Found: ' + qFound;
      }

      var gFound = zpSubgroup.getGenerator().getElementValue().toString();
      var gExpected = zpSubgroupRef.getGenerator().getElementValue().toString();
      if (!gFound.equals(gExpected)) {
        throw 'Expected generator: ' + gExpected +
            ' for Zp subgroup ; Found: ' + gFound;
      }
    },

    /**
     * Checks an exponents array against a reference exponents array.
     *
     * @param {Object}
     *            exponentsArray the exponents array to check.
     * @param {Object}
     *            exponentsArrayRef the reference exponents array.
     * @throws {Error}
     *             if the check fails.
     */
    checkExponentsArray: function(exponentsArray, exponentsArrayRef) {

      for (var i = 0; i < exponentsArray.length; ++i) {
        var exponentFound = exponentsArray[i].getValue().toString();
        var exponentExpected = exponentsArrayRef[i].getValue().toString();
        if (!exponentFound.equals(exponentExpected)) {
          throw 'Expected exponent ' + i +
              ' of array to be: ' + exponentExpected +
              ' ; Found: ' + exponentFound;
        }
      }
    },

    /**
     * Stringifies a collection of Zp group elements. NOTE: It should be
     * considered to add this function (and its corresponding
     * deserialization function) to cryptoLib and cryptolib-js and use the
     * resulting functions instead of this function and the function
     * "deserializeZpGroupElements" (defined below), to exchange collections
     * of Zp group elements between the 2 libraries.
     *
     * @param {String}
     *            zpGroupElements the collection of Zp group elements to
     *            serialize.
     * @return {ZpGroupElement[]} the serialized collection of Zp group
     *         elements.
     */
    stringifyZpGroupElements: function(zpGroupElements) {

      var p = _converters.base64FromBigInteger(zpGroupElements[0].getP());
      var q = _converters.base64FromBigInteger(zpGroupElements[0].getQ());

      var values = [];
      for (var i = 0; i < zpGroupElements.length; i++) {
        values[i] = _converters.base64FromBigInteger(
            zpGroupElements[i].getElementValue());
      }

      var jsonStr =
          JSON.stringify({zpGroupElements: {p: p, q: q, values: values}});

      return jsonStr;
    },

    /**
     * Deserializes a serialized collection of Zp group elements. NOTE: This
     * function should be removed after a deserialization method for the
     * "stringify" function defined above is implemented in cryptolib-js.
     *
     * @param {String}
     *            serializedZpGroupElements the serialized collection of Zp
     *            group elements to deserialize.
     * @return {ZpGroupElement[]} the deserialized collection of Zp group
     *         elements.
     */
    deserializeZpGroupElements: function(serializedZpGroupElements) {

      try {
        var zpGroupElements = [];
        cryptolib('commons.mathematical', function(box) {
          var zpGroupElementJsons = serializedZpGroupElements.split(
              MATHEMATICAL_COLLECTION_SERIALIZATION_DELIMITER);
          for (var i = 0; i < zpGroupElementJsons.length; i++) {
            var zpGroupElement =
                box.commons.mathematical.groupUtils.deserializeGroupElement(
                    zpGroupElementJsons[i]);
            zpGroupElements.push(zpGroupElement);
          }
        });

        return zpGroupElements;
      } catch (error) {
        throw testUtils.errorToJson(error);
      }
    }
  };
})();
