/*
 * 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.mathematical.groups.impl;

import java.math.BigInteger;

import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import com.google.common.testing.EqualsTester;
import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.utils.system.OperatingSystem;
import com.scytl.cryptolib.mathematical.groups.MathematicalGroup;
import com.scytl.cryptolib.mathematical.groups.utils.GroupUtils;
import com.scytl.cryptolib.primitives.securerandom.configuration.ConfigSecureRandomAlgorithmAndProvider;
import com.scytl.cryptolib.primitives.securerandom.configuration.SecureRandomPolicy;
import com.scytl.cryptolib.primitives.securerandom.factory.CryptoRandomInteger;
import com.scytl.cryptolib.primitives.securerandom.factory.SecureRandomFactory;

/**
 * Tests for {@link Exponent}.
 */
public class ExponentTest {

    private static BigInteger _smallP;

    private static BigInteger _largeP;

    private static BigInteger _smallQ;

    private static BigInteger _largeQ;

    private static BigInteger _smallG;

    private static BigInteger _largeG;

    private static MathematicalGroup<?> _smallGroup;

    private static MathematicalGroup<?> _largeGroup;

    private static CryptoRandomInteger _cryptoRandomInteger;

    @BeforeClass
    public static void setUp() throws GeneralCryptoLibException {

        _smallP = new BigInteger("23");

        _smallQ = new BigInteger("11");

        _smallG = new BigInteger("2");

        _smallGroup = new ZpSubgroup(_smallG, _smallP, _smallQ);

        _largeP = new BigInteger(
            "25878792566670842099842137716422866466252991028815773139028451679515364679624923581358662655689289205766441980239548823737806954397019411202244121935752456749381769565031670387914863935577896116425654849306598185507995737892509839616944496073707445338806101425467388977937489020456783676102620561970644684015868766028080049372849872115052208214439472603355483095640041515460851475971118272125133224007949688443680429668091313474118875081620746919907567682398209044343652147328622866834600839878114285018818463110227111614032671442085465843940709084719667865761125514800243342061732684028802646193202210299179139410607");

        _largeQ = new BigInteger(
            "12939396283335421049921068858211433233126495514407886569514225839757682339812461790679331327844644602883220990119774411868903477198509705601122060967876228374690884782515835193957431967788948058212827424653299092753997868946254919808472248036853722669403050712733694488968744510228391838051310280985322342007934383014040024686424936057526104107219736301677741547820020757730425737985559136062566612003974844221840214834045656737059437540810373459953783841199104522171826073664311433417300419939057142509409231555113555807016335721042732921970354542359833932880562757400121671030866342014401323096601105149589569705303");

        _largeG = new BigInteger(
            "23337993065784550228812110720552652305178266477392633588884900695706615523553977368516877521940228584865573144621632575456086035440118913707895716109366641541746808409917179478292952139273396531060021729985473121368590574110220870149822495151519706210399569901298027813383104891697930149341258267962490850297875794622068418425473578455187344232698462829084010585324877420343904740081787639502967515631687068869545665294697583750184911025514712871193837246483893950501015755683415509019863976071649325968623617568219864744389709563087949389080252971419711636380986100047871404548371112472694814597772988558887480308242");

        _largeGroup = new ZpSubgroup(_largeG, _largeP, _largeQ);

        SecureRandomPolicy secureRandomPolicy = getSecureRandomPolicy();
        _cryptoRandomInteger = new SecureRandomFactory(secureRandomPolicy)
            .createIntegerRandom();
    }

    @Rule
    public ExpectedException thrown = ExpectedException.none();

    @Test
    public void givenAJsonStringThenCreateExponent()
            throws GeneralCryptoLibException {

        String jsonStr =
            "{\"exponent\":{\"q\":\"Cw==\",\"value\":\"Cg==\"}}";

        Exponent exponent = Exponent.fromJson(jsonStr);

        Assert.assertEquals(exponent.getValue(), BigInteger.TEN);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenAMissingJsonStringFieldThenException()
            throws GeneralCryptoLibException {

        String jsonStr = "{\"exponent\":{\"q\":\"Cw==\"}}";

        Exponent.fromJson(jsonStr);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenNullGroupAndCryptoSecureRandomWhenAttemptToCreateExponentThenException()
            throws GeneralCryptoLibException {

        new GroupUtils().getRandomExponent(null, _cryptoRandomInteger);

    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenNullNullExponentWhenAttemptToCreateExponentThenException()
            throws GeneralCryptoLibException {

        new Exponent(_smallQ, null);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenNullQAndValidValueWhenAttemptToCreateExponentThenException()
            throws GeneralCryptoLibException {

        new Exponent(null, BigInteger.TEN);
    }

    @Test
    public void givenANonRandomExponentValueLessThanQGetExponentValue()
            throws GeneralCryptoLibException {

        BigInteger exponentValue = new BigInteger("1");
        BigInteger expectedExponentValue = exponentValue;

        createExponentAndAssertExponentValue(_smallQ, exponentValue,
            expectedExponentValue);
    }

    @Test
    public void givenANonRandomExponentValueGreaterThanQGetExponentValue()
            throws GeneralCryptoLibException {

        BigInteger exponentValue = new BigInteger("113");
        BigInteger expectedExponentValue = new BigInteger("3");

        createExponentAndAssertExponentValue(_smallQ, exponentValue,
            expectedExponentValue);
    }

    @Test
    public void givenANonRandomExponentValueEqualToQGetExponentValue()
            throws GeneralCryptoLibException {

        BigInteger exponentValue = new BigInteger("11");
        BigInteger expectedExponentValue = BigInteger.ZERO;

        createExponentAndAssertExponentValue(_smallQ, exponentValue,
            expectedExponentValue);
    }

    @Test
    public void givenANonRandomNegativeExponentGetExponentValue()
            throws GeneralCryptoLibException {

        BigInteger exponentValue = new BigInteger("-111");
        BigInteger expectedExponentValue = BigInteger.TEN;

        createExponentAndAssertExponentValue(_smallQ, exponentValue,
            expectedExponentValue);
    }

    @Test
    public void givenAnExponentWhenGetQThenExpectedQReturned()
            throws GeneralCryptoLibException {

        BigInteger exponentValue = new BigInteger("2");
        BigInteger expectedQ = new BigInteger("11");

        createExponentAndAssertQ(_smallQ, _smallP, _smallG, expectedQ,
            exponentValue);
    }

    @Test
    public void testWhenRandomExponentCreatedThenValueIsInRange()
            throws GeneralCryptoLibException {

        boolean notLessThanZero = false;
        boolean lessThanQ = false;

        Exponent randomExponent = new GroupUtils()
            .getRandomExponent(_smallGroup, _cryptoRandomInteger);

        if (BigInteger.ZERO.compareTo(randomExponent.getValue()) < 1) {
            notLessThanZero = true;
        }
        if (randomExponent.getValue().compareTo(_smallQ) == -1) {
            lessThanQ = true;
        }

        Assert.assertEquals(
            "The random exponent should be equal or greater than zero",
            true, notLessThanZero);
        Assert.assertEquals("The random exponent should be less than q",
            true, lessThanQ);
    }

    @Test
    public void testRandomExponents() throws GeneralCryptoLibException {

        GroupUtils groupsUtils = new GroupUtils();

        String errorMessage = "The random exponents should be different";

        Exponent exponent1 = groupsUtils.getRandomExponent(_largeGroup,
            _cryptoRandomInteger);
        Exponent exponent2 = groupsUtils.getRandomExponent(_largeGroup,
            _cryptoRandomInteger);
        Exponent exponent3 = groupsUtils.getRandomExponent(_largeGroup,
            _cryptoRandomInteger);

        Assert.assertNotEquals(errorMessage, exponent1.getValue(),
            exponent2.getValue());
        Assert.assertNotEquals(errorMessage, exponent1.getValue(),
            exponent3.getValue());
        Assert.assertNotEquals(errorMessage, exponent2.getValue(),
            exponent3.getValue());
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenExponentsDifferentGroupsWhenAddThenException()
            throws GeneralCryptoLibException {

        Exponent exponent1 = new Exponent(_smallQ, _smallG);
        Exponent exponent2 = new Exponent(_largeQ, _largeG);

        exponent1.add(exponent2);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenNullExponentsWhenAddThenException()
            throws GeneralCryptoLibException {

        Exponent exponent1 = new Exponent(_smallQ, _smallG);
        Exponent exponent2 = null;

        exponent1.add(exponent2);
    }

    @Test
    public void givenTwoExponentsWhenAddedThenLessThanQ()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = new BigInteger("2");
        BigInteger exponent2Value = new BigInteger("3");
        BigInteger expectedResult = new BigInteger("5");

        addExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenTwoExponentsWhenAddedThenEqualsToQ()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = new BigInteger("5");
        BigInteger exponent2Value = new BigInteger("6");
        BigInteger expectedResult = BigInteger.ZERO;

        addExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenTwoExponentsWhenAddedThenGreaterThanQ()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = BigInteger.TEN;
        BigInteger exponent2Value = new BigInteger("2");
        BigInteger expectedResult = BigInteger.ONE;

        addExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenTwoEqualExponentsWhenAddedThenGreaterThanQ()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = BigInteger.TEN;
        BigInteger exponent2Value = BigInteger.TEN;
        BigInteger expectedResult = new BigInteger("9");

        addExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenTwoExponentsOneEqualToZeroWhenAddedThenSucceeds()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = BigInteger.ZERO;
        BigInteger exponent2Value = new BigInteger("4");
        BigInteger expectedResult = new BigInteger("4");

        addExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenAnExponentWithValueZeroWhenNegatedThenResultIsZero()
            throws GeneralCryptoLibException {
        BigInteger exponentValue = BigInteger.ZERO;
        BigInteger expectedResult = BigInteger.ZERO;

        negateExponentAndAssert(exponentValue, expectedResult);
    }

    @Test
    public void givenAnExponentLessThanQWhenNegatedThenSucceeds()
            throws GeneralCryptoLibException {
        BigInteger exponentValue = new BigInteger("9");
        BigInteger expectedResult = new BigInteger("2");

        negateExponentAndAssert(exponentValue, expectedResult);
    }

    @Test
    public void givenTwoExponentsWhenSubtractedResultIsPositive()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = new BigInteger("3");
        BigInteger exponent2Value = new BigInteger("2");
        BigInteger expectedResult = BigInteger.ONE;

        subtractExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenTwoExponentsWhenSubtractedResultIsZero()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = BigInteger.TEN;
        BigInteger exponent2Value = BigInteger.TEN;
        BigInteger expectedResult = BigInteger.ZERO;

        subtractExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenTwoExponentsWhenSubtractedResultIsNegative()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = new BigInteger("2");
        BigInteger exponent2Value = new BigInteger("3");
        BigInteger expectedResult = BigInteger.TEN;

        subtractExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenAnExponentWhenSubtractedZeroThenResultIsTheExponent()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = new BigInteger("4");
        BigInteger exponent2Value = BigInteger.ZERO;
        BigInteger expectedResult = new BigInteger("4");

        subtractExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenAnExponentWhenMultipliedSmallThenResultIsCorrect()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = BigInteger.valueOf(2);
        BigInteger exponent2Value = BigInteger.valueOf(3);
        BigInteger expectedResult = BigInteger.valueOf(6);

        multiplyExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenAnExponentWhenMultipliedBigThenResultIsCorrect()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = BigInteger.valueOf(2);
        BigInteger exponent2Value = BigInteger.valueOf(6);
        BigInteger expectedResult = BigInteger.ONE;

        multiplyExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenAnExponentWhenMultipliedOneThenResultIsZero()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = BigInteger.valueOf(2);
        BigInteger exponent2Value = BigInteger.ONE;
        BigInteger expectedResult = BigInteger.valueOf(2);

        multiplyExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    @Test
    public void givenAnExponentWhenMultipliedZeroThenResultIsZero()
            throws GeneralCryptoLibException {
        BigInteger exponent1Value = BigInteger.valueOf(2);
        BigInteger exponent2Value = BigInteger.ZERO;
        BigInteger expectedResult = BigInteger.ZERO;

        multiplyExponentsAndAssert(exponent1Value, exponent2Value,
            expectedResult);
    }

    /**
     * Creates an exponent and then calls the toJson() method on that exponent.
     * Asserts that the returned string can be used to reconstruct the exponent.
     *
     * @throws GeneralCryptoLibException
     */
    @Test
    public void givenAnExponentWhenToJsonCanRecoverExponent()
            throws GeneralCryptoLibException {

        Exponent exponent = new Exponent(_smallQ, BigInteger.TEN);

        String jsonStr = exponent.toJson();

        Exponent reconstructedExponent = Exponent.fromJson(jsonStr);

        String errorMessage =
            "The Exponent could not be reconstructed from a JSON string.";
        Assert.assertEquals(errorMessage, reconstructedExponent, exponent);
    }

    /**
     * Tests the equals() and hashcode() methods using
     * {@link com.google.common.testing.EqualsTester}.
     *
     * @throws GeneralCryptoLibException
     */
    @Test
    public void testEquals() throws GeneralCryptoLibException {

        ZpSubgroup mathematicalGroup_q11 =
            new ZpSubgroup(new BigInteger("2"), new BigInteger("23"),
                new BigInteger("11"));
        ZpSubgroup mathematicalGroup_q3 =
            new ZpSubgroup(new BigInteger("2"), new BigInteger("23"),
                new BigInteger("3"));

        Exponent exponent1_q11_value10 =
            new Exponent(mathematicalGroup_q11.getQ(), BigInteger.TEN);
        Exponent exponent2_q11_value10 =
            new Exponent(mathematicalGroup_q11.getQ(), BigInteger.TEN);

        Exponent exponent3_q13_value4 =
            new Exponent(mathematicalGroup_q3.getQ(), new BigInteger("4"));

        Exponent exponent4_q11_value9 = new Exponent(
            mathematicalGroup_q11.getQ(), new BigInteger("9"));

        new EqualsTester()
            .addEqualityGroup(exponent1_q11_value10, exponent2_q11_value10)
            .addEqualityGroup(exponent3_q13_value4)
            .addEqualityGroup(exponent4_q11_value9).testEquals();
    }
    
    @Test
    public void testToString() throws GeneralCryptoLibException {
        Exponent element = new Exponent(BigInteger.TEN, BigInteger.valueOf(2));
        String toString = element.toString();
        Assert.assertTrue(toString.contains("=2"));
        Assert.assertTrue(toString.contains("=" + BigInteger.TEN.toString()));
    }

    /**
     * @param q
     *            the Zp subgroup q parameter.
     * @param exponentValue
     *            The desired exponent value.
     * @param expectedExponentValue
     *            The expected exponent value.
     * @throws GeneralCryptoLibException
     */
    private void createExponentAndAssertExponentValue(final BigInteger q,
            final BigInteger exponentValue,
            final BigInteger expectedExponentValue)
            throws GeneralCryptoLibException {

        Exponent exponent = new Exponent(q, exponentValue);

        Assert.assertEquals("The exponent value is not the expected one",
            expectedExponentValue, exponent.getValue());
    }

    /**
     * @param q
     *            The q parameter to be used when creating the exponent.
     * @param expectedQ
     *            The q that is expected to be returned when the getQ() method
     *            is called.
     * @param exponentValue
     *            The exponent value to be used when creating the exponent.
     * @throws GeneralCryptoLibException
     */
    private void createExponentAndAssertQ(final BigInteger q,
            final BigInteger p, final BigInteger g,
            final BigInteger expectedQ, final BigInteger exponentValue)
            throws GeneralCryptoLibException {

        Exponent exponent = new Exponent(q, exponentValue);

        Assert.assertEquals("The q is not the expected one", expectedQ,
            exponent.getQ());
    }

    /**
     * Create the exponents with the given values, and add them. Then assert
     * that the exponent result has the expected value.
     *
     * @param exponent1Value
     *            The exponent1 value.
     * @param exponent2Value
     *            The exponent2 value.
     * @param expectedResult
     *            The expected value of the result of adding those two
     *            exponents.
     * @throws GeneralCryptoLibException
     */
    private void addExponentsAndAssert(final BigInteger exponent1Value,
            final BigInteger exponent2Value,
            final BigInteger expectedResult)
            throws GeneralCryptoLibException {

        Exponent exponent1 = new Exponent(_smallQ, exponent1Value);
        Exponent exponent2 = new Exponent(_smallQ, exponent2Value);

        exponent1 = exponent1.add(exponent2);

        Assert.assertEquals("The operation result is invalid",
            expectedResult, exponent1.getValue());
    }

    /**
     * Create the exponents with the given values, and subtract them:
     * {@code (exponent1 - exponent2)}. Then assert that the exponent result has
     * the expected value
     *
     * @param exponent1Value
     *            The exponent1 value.
     * @param exponent2Value
     *            The exponent2 value.
     * @param expectedResult
     *            The expected value of the result of subtracting those two
     *            exponents: {@code (exponent1 - exponent2)}.
     * @throws GeneralCryptoLibException
     *             if arguments are invalid.
     */
    private void subtractExponentsAndAssert(
            final BigInteger exponent1Value,
            final BigInteger exponent2Value,
            final BigInteger expectedResult)
            throws GeneralCryptoLibException {

        Exponent exponent1 = new Exponent(_smallQ, exponent1Value);

        Exponent exponent2 = new Exponent(_smallQ, exponent2Value);

        exponent1 = exponent1.subtract(exponent2);

        Assert.assertEquals("The operation result is invalid",
            expectedResult, exponent1.getValue());
    }

    /**
     * Create the exponents with the given values, and multiplies them:
     * {@code (exponent1 * exponent2)}. Then assert that the exponent result has
     * the expected value
     *
     * @param exponent1Value
     *            The exponent1 value.
     * @param exponent2Value
     *            The exponent2 value.
     * @param expectedResult
     *            The expected value of the result of multiplying those two
     *            exponents: {@code (exponent1 * exponent2)}.
     * @throws GeneralCryptoLibException
     *             if arguments are invalid.
     */
    private void multiplyExponentsAndAssert(
            final BigInteger exponent1Value,
            final BigInteger exponent2Value,
            final BigInteger expectedResult)
            throws GeneralCryptoLibException {
        Exponent exponent1 = new Exponent(_smallQ, exponent1Value);

        Exponent exponent2 = new Exponent(_smallQ, exponent2Value);

        exponent1 = exponent1.multiply(exponent2);

        Assert.assertEquals("The operation result is invalid",
            expectedResult, exponent1.getValue());
    }

    /**
     * Creates the exponent with the {@code exponentValue}, and negate it.
     * Asserts that the negated exponent has the {@code expectedValue}.
     *
     * @param exponentValue
     *            The value for the exponent.
     * @param expectedValue
     *            The expected value for the negated exponent.
     * @throws GeneralCryptoLibException
     *             if arguments are invalid.
     */
    private void negateExponentAndAssert(final BigInteger exponentValue,
            final BigInteger expectedValue)
            throws GeneralCryptoLibException {
        Exponent exponent = new Exponent(_smallQ, exponentValue);
        Exponent negated = exponent.negate();

        Assert.assertEquals(
            "The negated exponent has not the expected value",
            expectedValue, negated.getValue());
    }

    private static SecureRandomPolicy getSecureRandomPolicy() {

        return new SecureRandomPolicy() {

            @Override
            public ConfigSecureRandomAlgorithmAndProvider getSecureRandomAlgorithmAndProvider() {

                switch (OperatingSystem.current()) {

                case WINDOWS:
                    return ConfigSecureRandomAlgorithmAndProvider.PRNG_SUN_MSCAPI;
                case LINUX:
                    return ConfigSecureRandomAlgorithmAndProvider.NATIVE_PRNG_SUN;
                default:
                    throw new CryptoLibException("OS not supported");
                }
            }
        };
    }
}
