/*
 * 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
 */
package com.scytl.cryptolib.interoperation.js.commands.digital.envelope;

import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;

import javax.script.ScriptException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.utils.AsymmetricTestDataGenerator;
import com.scytl.cryptolib.digital.envelope.factory.CryptoDigitalEnvelope;
import com.scytl.cryptolib.digital.envelope.service.DigitalEnvelopeService;
import com.scytl.cryptolib.interoperation.js.commands.Command;
import com.scytl.cryptolib.interoperation.js.commands.CommandException;
import com.scytl.cryptolib.interoperation.js.constants.CommonConstants;
import com.scytl.cryptolib.interoperation.js.constants.DigitalEnvelopeConstants;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.cryptolib.primitives.primes.utils.PrimitivesTestDataGenerator;
import com.scytl.cryptolib.primitives.securerandom.constants.SecureRandomConstants;
import com.scytl.cryptolib.test.tools.utils.CommonTestDataGenerator;

/**
 * Tests the Java-JavaScript interoperation for digital envelope applications.
 */
public class TestDigitalEnvelope extends Command {

    private static final Logger LOGGER =
        LoggerFactory.getLogger(TestDigitalEnvelope.class);

    private final Object _namespace;

    private final DigitalEnvelopeService _digitalEnvelopeService;

    private byte[] _data;

    private String _dataB64;

    private PublicKey[] _publicKeys;

    private String[] _publicKeyPems;

    private PrivateKey[] _privateKeys;

    private String[] _privateKeyPems;

    /**
     * Default constructor.
     *
     * @throws CommandException
     *             if the script engine cannot evaluate the digital envelope
     *             interoperation tests implemented in JavaScript.
     */
    public TestDigitalEnvelope() throws CommandException {
        super(DigitalEnvelopeConstants.JS_DIGITAL_ENVELOPE_TEST_PATH);

        _namespace = _engine.get(
            DigitalEnvelopeConstants.JS_DIGITAL_ENVELOPE_TEST_NAMESPACE);

        clearExceptionData();

        try {
            generateInputData();

            _digitalEnvelopeService = new DigitalEnvelopeService();
        } catch (GeneralCryptoLibException e) {
            throw new CommandException.Builder().addCause(e).build();
        }
    }

    /**
     * Generates a digital envelope from some data using Java, opens the result
     * using JavaScript, generates a digital envelope for the same data using
     * JavaScript and opens the result using Java. If no exceptions are thrown
     * and the original data stored in the digital envelopes can be be
     * retrieved, then the interoperation test for digital envelope applications
     * was successful.
     *
     * @throws CommandException
     *             if a JavaScript method cannot be found, a cryptographic
     *             operation fails or a digital envelope opening operation
     *             yields data that does not correspond to the original data.
     */
    @Override
    public void execute() throws CommandException {

        String digitalEnvelopeJson = generateUsingJava();

        openUsingJavaScript(digitalEnvelopeJson);

        digitalEnvelopeJson = generateUsingJavaScript();

        openUsingJava(digitalEnvelopeJson);

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "INTEROPERATION TEST FOR DIGITAL ENVELOPE WAS SUCCESSFUL.");
    }

    private void generateInputData() throws GeneralCryptoLibException {

        _data = PrimitivesTestDataGenerator
            .getByteArray(CommonTestDataGenerator.getInt(1,
                SecureRandomConstants.MAXIMUM_GENERATED_BYTE_ARRAY_LENGTH));
        _dataB64 = Base64.getEncoder().encodeToString(_data);
        putExceptionData(
            "Data to store in digital envelope (Base64 encoded) [Java]",
            _dataB64);

        _publicKeys =
            new PublicKey[DigitalEnvelopeConstants.NUM_ASYMMETRIC_KEY_PAIRS];
        _publicKeyPems =
            new String[DigitalEnvelopeConstants.NUM_ASYMMETRIC_KEY_PAIRS];
        _privateKeys =
            new PrivateKey[DigitalEnvelopeConstants.NUM_ASYMMETRIC_KEY_PAIRS];
        _privateKeyPems =
            new String[DigitalEnvelopeConstants.NUM_ASYMMETRIC_KEY_PAIRS];
        for (int i =
            0; i < DigitalEnvelopeConstants.NUM_ASYMMETRIC_KEY_PAIRS; i++) {
            KeyPair keyPair =
                AsymmetricTestDataGenerator.getKeyPairForEncryption();

            _publicKeys[i] = keyPair.getPublic();
            _publicKeyPems[i] = PemUtils.publicKeyToPem(_publicKeys[i]);
            putExceptionData(
                "Public key " + (i + 1)
                    + " used to store data in digital envelope (PEM format) [Java]",
                _publicKeyPems[i]);

            _privateKeys[i] = keyPair.getPrivate();
            _privateKeyPems[i] = PemUtils.privateKeyToPem(_privateKeys[i]);
            putExceptionData(
                "Private key " + (i + 1)
                    + " used to retrieve data from digital envelope (PEM format) [Java]",
                _privateKeyPems[i]);
        }
    }

    private String generateUsingJava() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating digital envelope, using Java...");

        try {
            return _digitalEnvelopeService
                .createDigitalEnvelope(_data, _publicKeys).toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate digital envelope, using Java.", e);
        }
    }

    private void openUsingJavaScript(final String digitalEnvelopeJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Opening digital envelope, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                DigitalEnvelopeConstants.JS_DIGITAL_ENVELOPE_OPENING_FUNCTION,
                digitalEnvelopeJson, _privateKeyPems, _dataB64);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + DigitalEnvelopeConstants.JS_DIGITAL_ENVELOPE_TEST_NAMESPACE
                        + "."
                        + DigitalEnvelopeConstants.JS_DIGITAL_ENVELOPE_OPENING_FUNCTION
                        + " in resources file "
                        + DigitalEnvelopeConstants.JS_DIGITAL_ENVELOPE_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not open Java generated digital envelope, using JavaScript.",
                e);
        }
    }

    private String generateUsingJavaScript() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating digital envelope, using JavaScript...");

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                DigitalEnvelopeConstants.JS_DIGITAL_ENVELOPE_GENERATION_FUNCTION,
                _dataB64, _publicKeyPems);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + DigitalEnvelopeConstants.JS_DIGITAL_ENVELOPE_TEST_NAMESPACE
                        + "."
                        + DigitalEnvelopeConstants.JS_DIGITAL_ENVELOPE_GENERATION_FUNCTION
                        + " in resources file "
                        + DigitalEnvelopeConstants.JS_DIGITAL_ENVELOPE_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate digital envelope data, using JavaScript.",
                e);
        }
    }

    private void openUsingJava(final String digitalEnvelopeJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Opening digital envelope, using Java...");

        putExceptionData("Digital envelope (JSON format) [JS]",
            digitalEnvelopeJson);

        CryptoDigitalEnvelope digitalEnvelope;
        try {
            digitalEnvelope =
                CryptoDigitalEnvelope.fromJson(digitalEnvelopeJson);
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not deserialize JavaScript generated digital envelope",
                e);
        }

        try {
            for (int i =
                0; i < DigitalEnvelopeConstants.NUM_ASYMMETRIC_KEY_PAIRS; i++) {
                String decryptedDataB64 =
                    Base64.getEncoder().encodeToString(
                        _digitalEnvelopeService.openDigitalEnvelope(
                            digitalEnvelope, _privateKeys[i]));
                putExceptionData(
                    "Faulty data retrieved from digital envelope (Base64 encoded) [Java]",
                    decryptedDataB64);
                putExceptionData(
                    "Private key used to retrieve faulty data from digital envelope (PEM format) [Java]",
                    _privateKeyPems[i]);

                if (!decryptedDataB64.equals(_dataB64)) {
                    throw new GeneralCryptoLibException(
                        "Expected data retrieved from digital envelope (Base64 encoded): "
                            + _dataB64 + " ; Found: " + decryptedDataB64);
                }
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not open JavaScript generated digital envelope, using Java.",
                e);
        }
    }
}
