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

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.util.Arrays;

import javax.crypto.SecretKey;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.constants.Constants;
import com.scytl.cryptolib.primitives.securerandom.configuration.SecureRandomPolicyFromProperties;
import com.scytl.cryptolib.primitives.securerandom.factory.CryptoRandomBytes;
import com.scytl.cryptolib.primitives.securerandom.factory.SecureRandomFactory;
import com.scytl.cryptolib.symmetric.mac.configuration.MacPolicyFromProperties;
import com.scytl.cryptolib.symmetric.service.SymmetricService;

public class MacGenerationTest {
    /**
     * An int of value zero.
     */
    public static final int ZERO = 0;

    /**
     * An int of value one.
     */
    public static final int ONE = 1;

    /**
     * An int of value two.
     */
    public static final int TWO = 2;

    private static final int DATA_BYTE_LENGTH = 10;

    private static final int NUMBER_OF_DATA_PARTS = 10;

    private final int MAC_BYTE_LENGTH = 32;

    private static SecretKey _key;

    private static CryptoRandomBytes _randomByteGenerator;

    private static byte[] _data1;

    private static byte[] _data2;

    private static byte[][] _dataParts1;

    private static byte[][] _dataParts2;

    private static CryptoMac _cryptoMac;

    @BeforeClass
    public static void setUp() throws GeneralCryptoLibException {

        Security.addProvider(new BouncyCastleProvider());

        SymmetricService symmetricServiceFromDefaultConstructor =
            new SymmetricService();

        _key =
            symmetricServiceFromDefaultConstructor.getSecretKeyForHmac();

        _randomByteGenerator =
            new SecureRandomFactory(new SecureRandomPolicyFromProperties(
                Constants.CRYPTOLIB_POLICY_PROPERTIES_FILE_PATH))
                    .createByteRandom();

        _data1 = _randomByteGenerator.nextRandom(DATA_BYTE_LENGTH);

        _data2 = _randomByteGenerator.nextRandom(DATA_BYTE_LENGTH + ONE);

        _dataParts1 = new byte[NUMBER_OF_DATA_PARTS][];
        for (int i = 0; i < NUMBER_OF_DATA_PARTS; i++) {
            _dataParts1[i] =
                _randomByteGenerator.nextRandom(DATA_BYTE_LENGTH);
        }

        _dataParts2 = new byte[NUMBER_OF_DATA_PARTS][];
        for (int i = 0; i < NUMBER_OF_DATA_PARTS; i++) {
            _dataParts2[i] =
                _randomByteGenerator.nextRandom(DATA_BYTE_LENGTH + ONE);
        }

        _cryptoMac = new MacFactory(new MacPolicyFromProperties(
            Constants.CRYPTOLIB_POLICY_PROPERTIES_FILE_PATH)).create();
    }

    @AfterClass
    public static void tearDown() {
        Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
    }

    @Test
    public void whenGenerateMacFromBytesThenHasExpectedLength() {

        byte[] mac = _cryptoMac.generate(_key, _data1);

        int macByteLength = mac.length;

        assertEquals(macByteLength, MAC_BYTE_LENGTH);
    }

    @Test
    public void whenGenerateMacFromStreamThenHasExpectedLength()
            throws GeneralCryptoLibException {

        byte[] mac = _cryptoMac.generate(_key,
            constructStreamContainingBytes(1024));

        int macByteLength = mac.length;

        assertEquals(macByteLength, MAC_BYTE_LENGTH);
    }

    @Test
    public void whenGenerateMacFromSameDataTwiceThenSameMac() {

        byte[] mac1 = _cryptoMac.generate(_key, _data1);

        byte[] mac2 = _cryptoMac.generate(_key, _data1);

        assertTrue(Arrays.equals(mac1, mac2));
    }

    @Test
    public void whenGenerateMacFromNoData() {
        _cryptoMac.generate(_key);
    }

    @Test
    public void whenGenerateMacFromNullData() {
        byte[] data = null;
        _cryptoMac.generate(_key, data);
    }

    @Test(expected = CryptoLibException.class)
    public void whenGenerateMacWithNullKeyThenException() {
        _cryptoMac.generate(null, _data1);
    }

    @Test
    public void whenGenerateMacWithOtherKey() {
        SecretKey key = new SecretKey() {
            private static final long serialVersionUID =
                -1360480738312503962L;

            @Override
            public String getFormat() {
                return null;
            }

            @Override
            public byte[] getEncoded() {
                return "0123".getBytes();
            }

            @Override
            public String getAlgorithm() {
                return null;
            }
        };
        _cryptoMac.generate(key, _data1);
    }

    @Test(expected = CryptoLibException.class)
    public void whenGenerateMacWithNullKeyByteArray() {
        SecretKey key = new SecretKey() {
            private static final long serialVersionUID =
                4144119085977660815L;

            @Override
            public String getFormat() {
                return null;
            }

            @Override
            public byte[] getEncoded() {
                return null;
            }

            @Override
            public String getAlgorithm() {
                return null;
            }
        };
        _cryptoMac.generate(key, _data1);
    }

    @Test
    public void whenGenerateMacsForDifferentDataThenDifferentMacs() {

        byte[] mac1 = _cryptoMac.generate(_key, _data1);

        byte[] mac2 = _cryptoMac.generate(_key, _data2);

        assertFalse(Arrays.equals(mac1, mac2));
    }

    @Test
    public void whenGenerateMacsForSameDataThenSameMacs() {

        byte[] mac1 = _cryptoMac.generate(_key, _dataParts1);

        byte[] mac2 = _cryptoMac.generate(_key, _dataParts1);

        assertTrue(Arrays.equals(mac1, mac2));
    }

    @Test
    public void whenGenerateMacsForDifferentArraysOfDataByteArraysThenDifferentMacs() {

        byte[] mac1 = _cryptoMac.generate(_key, _dataParts1);

        byte[] mac2 = _cryptoMac.generate(_key, _dataParts2);

        assertFalse(Arrays.equals(mac1, mac2));
    }

    @Test
    public void whenGenerateMacsForSameArraysOfDataByteArraysThenSameMacs() {

        byte[] mac1 = _cryptoMac.generate(_key, _dataParts1);

        byte[] mac2 = _cryptoMac.generate(_key, _dataParts1);

        assertTrue(Arrays.equals(mac1, mac2));
    }

    @Test
    public void whenGenerateMacsForDifferentCommaSeparatedListsOfDataByteArraysThenDifferentMacs() {

        byte[] mac1 = _cryptoMac.generate(_key, _dataParts1[ZERO],
            _dataParts1[ONE], _dataParts1[TWO]);

        byte[] mac2 = _cryptoMac.generate(_key, _dataParts1[ZERO],
            _dataParts1[ONE], _dataParts2[TWO]);

        assertFalse(Arrays.equals(mac1, mac2));
    }

    @Test
    public void whenGenerateMacsForSameCommaSeparatedListsOfDataByteArraysThenSameMacs() {

        byte[] mac1 = _cryptoMac.generate(_key, _dataParts1[ZERO],
            _dataParts1[ONE], _dataParts1[TWO]);

        byte[] mac2 = _cryptoMac.generate(_key, _dataParts1[ZERO],
            _dataParts1[ONE], _dataParts1[TWO]);

        assertTrue(Arrays.equals(mac1, mac2));
    }

    @Test
    public void whenGenerateAndVerifyMacFromStreamThenTrue()
            throws GeneralCryptoLibException {

        byte[] mac = _cryptoMac.generate(_key,
            constructStreamContainingBytes(1024));

        assertTrue(_cryptoMac.verify(_key, mac,
            constructStreamContainingBytes(1024)));
    }

    @Test
    public void whenVerifyBadMacThenFalse()
            throws GeneralCryptoLibException {

        byte[] badMac = new byte[MAC_BYTE_LENGTH];

        assertFalse(_cryptoMac.verify(_key, badMac,
            constructStreamContainingBytes(1024)));
    }

    @Test
    public void whenGenerateMacFromStreamTwiceThenSameMacs()
            throws GeneralCryptoLibException {

        byte[] mac1 = _cryptoMac.generate(_key,
            constructStreamContainingBytes(1024));

        byte[] mac2 = _cryptoMac.generate(_key,
            constructStreamContainingBytes(1024));

        assertTrue(Arrays.equals(mac1, mac2));
    }

    @Test
    public void whenGenerateMacFromStreamAndFromBytesThenSameMacs()
            throws GeneralCryptoLibException, IOException {

        // generate a MAC from stream
        byte[] macFromStream = _cryptoMac.generate(_key,
            constructStreamContainingBytes(1024));

        // generate a MAC from all the bytes at once
        byte[] allBytesFromFile = constructBytes(1024);
        byte[] macFromBytes = _cryptoMac.generate(_key, allBytesFromFile);

        // confirm that both MACs are equal
        assertTrue(Arrays.equals(macFromStream, macFromBytes));
    }

    @Test
    public void whenGenerateMacFromBytesAndVerifyUsingStreamThenTrue()
            throws GeneralCryptoLibException, IOException {

        // generate a MAC from all the bytes at once
        byte[] allBytesFromFile = constructBytes(1024);
        byte[] macFromBytes = _cryptoMac.generate(_key, allBytesFromFile);

        // confirm that both MACs are equal
        assertTrue(_cryptoMac.verify(_key, macFromBytes,
            constructStreamContainingBytes(1024)));
    }

    @Test
    public void whenGenerateMacFromStreamAndVerifyUsingBytesThenTrue()
            throws GeneralCryptoLibException, IOException {

        // generate a MAC from stream
        byte[] macFromStream = _cryptoMac.generate(_key,
            constructStreamContainingBytes(1024));

        // read all of the bytes that will be used to verify the mac
        byte[] allBytesFromFile = constructBytes(1024);

        // confirm that both MACs are equal
        assertTrue(
            _cryptoMac.verify(_key, macFromStream, allBytesFromFile));
    }

    private byte[] constructBytes(int count) {
        byte[] bytes = new byte[count];
        for (int i = 0; i < bytes.length; i++) {
            bytes[i] = (byte) i;
        }
        return bytes;
    }

    private InputStream constructStreamContainingBytes(final int numberBytes) {
        return new ByteArrayInputStream(constructBytes(numberBytes));
    }
}
