/*
 * 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.symmetric;

import java.util.Base64;

import javax.crypto.SecretKey;
import javax.script.ScriptException;

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

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
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.SymmetricConstants;
import com.scytl.cryptolib.primitives.primes.utils.PrimitivesTestDataGenerator;
import com.scytl.cryptolib.primitives.securerandom.constants.SecureRandomConstants;
import com.scytl.cryptolib.symmetric.service.SymmetricService;
import com.scytl.cryptolib.test.tools.utils.CommonTestDataGenerator;

/**
 * Tests the Java-JavaScript interoperation for symmetric cipher applications.
 */
public class TestSymmetricCipher extends Command {

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

    private final Object _namespace;

    private final SymmetricService _symmetricService;

    private SecretKey _secretKey;

    private String _secretKeyB64;

    private byte[] _data;

    private String _dataB64;

    /**
     * Default constructor.
     *
     * @throws CommandException
     *             if the script engine cannot evaluate the symmetric cipher
     *             interoperation tests implemented in JavaScript.
     */
    public TestSymmetricCipher() throws CommandException {
        super(SymmetricConstants.JS_SYMMETRIC_CIPHER_TEST_PATH);

        _namespace = _engine
            .get(SymmetricConstants.JS_SYMMETRIC_CIPHER_TEST_NAMESPACE);

        clearExceptionData();

        try {
            _symmetricService = new SymmetricService();

            generateInputData();
        } catch (GeneralCryptoLibException e) {
            throw new CommandException.Builder().addCause(e).build();
        }
    }

    /**
     * Symmetrically encrypts some data using Java, decrypts the result using
     * JavaScript, encrypts the same data using JavaScript and decrypts the
     * result using Java. If no exceptions are thrown and the original data can
     * be retrieved, then the interoperation test for symmetric cipher
     * applications was successful.
     *
     * @throws CommandException
     *             if a JavaScript method cannot be found, a cryptographic
     *             operation fails or a decryption operation yields data that
     *             does not correspond to the original data.
     */
    @Override
    public void execute() throws CommandException {

        String encryptedDataB64 = encryptUsingJava();

        decryptUsingJavaScript(encryptedDataB64);

        encryptedDataB64 = encryptUsingJavaScript();

        decryptUsingJava(encryptedDataB64);

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

    private void generateInputData() throws GeneralCryptoLibException {

        _secretKey = _symmetricService.getSecretKeyForEncryption();
        _secretKeyB64 =
            Base64.getEncoder().encodeToString(_secretKey.getEncoded());
        putExceptionData("Secret Key (Base64 encoded) [Java]",
            _secretKeyB64);

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

    private String encryptUsingJava() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Symmetrically encrypting data, using Java...");

        try {
            return Base64.getEncoder().encodeToString(
                _symmetricService.encrypt(_secretKey, _data));
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not symmetrically encrypt data, using Java.", e);
        }
    }

    private void decryptUsingJavaScript(final String encryptedDataB64)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Symmetrically decrypting data, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                SymmetricConstants.JS_SYMMETRIC_DECRYPTION_FUNCTION,
                _secretKeyB64, encryptedDataB64, _dataB64);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + SymmetricConstants.JS_SYMMETRIC_CIPHER_TEST_NAMESPACE
                        + "."
                        + SymmetricConstants.JS_SYMMETRIC_DECRYPTION_FUNCTION
                        + " in resources file "
                        + SymmetricConstants.JS_SYMMETRIC_CIPHER_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not symmetrically decrypt Java encrypted data, using JavaScript.",
                e);
        }
    }

    private String encryptUsingJavaScript() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Symmetrically encrypting data, using JavaScript...");

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                SymmetricConstants.JS_SYMMETRIC_ENCRYPTION_FUNCTION,
                _secretKeyB64, _dataB64);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + SymmetricConstants.JS_SYMMETRIC_CIPHER_TEST_NAMESPACE
                        + "."
                        + SymmetricConstants.JS_SYMMETRIC_ENCRYPTION_FUNCTION
                        + " in resources file "
                        + SymmetricConstants.JS_SYMMETRIC_CIPHER_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not symmetrically encrypt data, using JavaScript.",
                e);
        }
    }

    private void decryptUsingJava(final String encryptedDataB64)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Symmetrically decrypting data, using Java...");

        putExceptionData("Encrypted data (Base64 encoded) [JS]",
            encryptedDataB64);

        try {
            byte[] decryptedData = _symmetricService.decrypt(_secretKey,
                Base64.getDecoder().decode(encryptedDataB64));
            String decryptedDataB64 =
                Base64.getEncoder().encodeToString(decryptedData);
            putExceptionData("Decrypted data (Base64 encoded) [Java]",
                decryptedDataB64);

            if (!decryptedDataB64.equals(_dataB64)) {
                throw new GeneralCryptoLibException(
                    "Expected decrypted data (Base64 encoded): " + _dataB64
                        + " ; Found: " + decryptedDataB64);
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not symmetrically decrypt JavaScript encrypted data, using Java.",
                e);
        }
    }
}
