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

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.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.PrimitiveConstants;
import com.scytl.cryptolib.primitives.primes.utils.PrimitivesTestDataGenerator;
import com.scytl.cryptolib.primitives.securerandom.constants.SecureRandomConstants;
import com.scytl.cryptolib.primitives.service.PrimitivesService;
import com.scytl.cryptolib.test.tools.utils.CommonTestDataGenerator;

/**
 * Tests the Java-JavaScript interoperation for hash applications.
 */
public class TestHash extends Command {

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

    private final Object _namespace;

    private final PrimitivesService _primitivesService;

    private byte[] _data;

    private String _dataB64;

    private String _hashB64;

    /**
     * Default constructor.
     *
     * @throws CommandException
     *             if the script engine cannot evaluate the hash interoperation
     *             tests implemented in JavaScript.
     */
    public TestHash() throws CommandException {
        super(PrimitiveConstants.JS_HASH_TEST_PATH);

        _namespace =
            _engine.get(PrimitiveConstants.JS_HASH_TEST_NAMESPACE);

        try {
            _primitivesService = new PrimitivesService();

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

    /**
     * Generates a hash using Java, verifies the result using JavaScript,
     * generates the same hash using JavaScript and verifies the result using
     * Java. If no exceptions are thrown and the same hash is generated both
     * times, then the interoperation test for hash applications was successful.
     *
     * @throws CommandException
     *             if a JavaScript method cannot be found, a cryptographic
     *             operation fails or a hash cannot be verified.
     */
    @Override
    public void execute() throws CommandException {

        String hashB64 = generateUsingJava();

        verifyUsingJavaScript(hashB64);

        hashB64 = generateUsingJavaScript();

        verifyUsingJava(hashB64);

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "INTEROPERATION TEST FOR HASH 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 hash (Base64 encoded) [Java]", _dataB64);
    }

    private String generateUsingJava() throws CommandException {

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

        try {
            _hashB64 = Base64.getEncoder()
                .encodeToString(_primitivesService.getHash(_data));
            putExceptionData("Hash (Base64 encoded) [Java]", _hashB64);

            return _hashB64;
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate hash, using Java.", e);
        }
    }

    private void verifyUsingJavaScript(final String hashB64)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying hash, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                PrimitiveConstants.JS_HASH_VERIFICATION_FUNCTION, hashB64,
                _dataB64);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + PrimitiveConstants.JS_HASH_TEST_NAMESPACE + "."
                        + PrimitiveConstants.JS_HASH_VERIFICATION_FUNCTION
                        + " in resources file "
                        + PrimitiveConstants.JS_HASH_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not verify hash, using JavaScript.", e);
        }
    }

    private String generateUsingJavaScript() throws CommandException {

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

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                PrimitiveConstants.JS_HASH_GENERATION_FUNCTION, _dataB64);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + PrimitiveConstants.JS_HASH_TEST_NAMESPACE + "."
                        + PrimitiveConstants.JS_HASH_GENERATION_FUNCTION
                        + " in resources file "
                        + PrimitiveConstants.JS_HASH_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate hash, using JavaScript.", e);
        }
    }

    private void verifyUsingJava(final String hashB64)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying hash, using Java...");

        putExceptionData("Hash (Base64 encoded) [JS]", hashB64);

        try {
            if (!hashB64.equals(_hashB64)) {
                throw new GeneralCryptoLibException(
                    "Expected hash (Base64 encoded): " + _hashB64
                        + " ; Found: " + hashB64);
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not verify JavaScript generated hash, using Java.",
                e);
        }
    }
}
