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

import mockit.Injectable;
import mockit.Mock;
import mockit.MockUp;
import mockit.NonStrictExpectations;

public class ZpGroupElementTest {

    private static BigInteger _q;

    private static BigInteger _p;

    private static BigInteger _g;

    private static ZpSubgroup _group_g2_q11;

    @BeforeClass
    public static void setUp() throws GeneralCryptoLibException {

        _q = new BigInteger("11");

        _p = new BigInteger("23");

        _g = new BigInteger("2");

        _group_g2_q11 = new ZpSubgroup(_g, _p, _q);
    }

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

    @Test
    public void givenToJsonStringThenCanReconstructElement()
            throws GeneralCryptoLibException {

        ZpGroupElement element =
            new ZpGroupElement(new BigInteger("2"), _p, _q);

        String jsonStr = element.toJson();

        ZpGroupElement reconstructedElement =
            ZpGroupElement.fromJson(jsonStr);

        String errorMsg =
            "Reconstructed Zp group element does not equal original element.";
        Assert.assertEquals(errorMsg, reconstructedElement, element);
    }

    @Test
    public void givenAValueWhenAGroupElementIsCreatedWithThatValueThenHasThatValue()
            throws GeneralCryptoLibException {
        BigInteger value = new BigInteger("2");

        ZpGroupElement element = new ZpGroupElement(value, _group_g2_q11);

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

    @Test(expected = GeneralCryptoLibException.class)
    public void whenCreateAnElementWithValueZeroThenError()
            throws GeneralCryptoLibException {
        BigInteger value = BigInteger.ZERO;

        new ZpGroupElement(value, _group_g2_q11);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void whenCreateAnElementWithNegativeValueThenError()
            throws Exception {
        BigInteger value = new BigInteger("-1");

        new ZpGroupElement(value, _group_g2_q11);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void whenCreateAnElementWithValueGreaterThanPThenError()
            throws GeneralCryptoLibException {
        BigInteger value = new BigInteger("24");
        new ZpGroupElement(value, _group_g2_q11);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void whenCreateAnElementWithNullValueThenError()
            throws GeneralCryptoLibException {
        new ZpGroupElement(null, _group_g2_q11);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void whenCreateAnElementWithNullGroupThenError()
            throws GeneralCryptoLibException {
        BigInteger value = BigInteger.ONE;
        ZpSubgroup group = null;
        new ZpGroupElement(value, group);
    }

    @Test
    public void whenCreateAnElement() throws GeneralCryptoLibException {
        BigInteger value = new BigInteger("17");
        new ZpGroupElement(value, _group_g2_q11);
    }

    @Test
    public void whenCreateAnElementWithValueOneThenResultHasValueOne()
            throws GeneralCryptoLibException {
        BigInteger value = BigInteger.ONE;

        ZpGroupElement element = new ZpGroupElement(value, _group_g2_q11);

        Assert.assertEquals("The result has a wrong value", value,
            element.getValue());
    }

    @Test
    public void givenAnElementWhenInvertedThenSucceeds()
            throws GeneralCryptoLibException {

        BigInteger value = new BigInteger("16");
        BigInteger expectedInverseValue = new BigInteger("13");

        invertAndAssert(value, expectedInverseValue);
    }

    @Test
    public void givenAnElementWithValueOneWhenInvertedThenResultIsOne()
            throws GeneralCryptoLibException {

        BigInteger value = BigInteger.ONE;
        BigInteger expectedInverseValue = BigInteger.ONE;

        invertAndAssert(value, expectedInverseValue);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenNullElementWhenMultiplyThenException()
            throws GeneralCryptoLibException {
        BigInteger value1 = new BigInteger("3");
        ZpGroupElement element1 =
            new ZpGroupElement(value1, _group_g2_q11);

        element1.multiply(null);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenTwoElementsFromDifferentGroupsWhenMultiplyThenException()
            throws GeneralCryptoLibException {
        BigInteger value1 = new BigInteger("3");
        BigInteger value2 = new BigInteger("3");

        ZpGroupElement element1 =
            new ZpGroupElement(value1, _group_g2_q11);
        ZpGroupElement element2 = new ZpGroupElement(value2,
            new ZpSubgroup(_g, new BigInteger("7"), new BigInteger("3")));
        element1.multiply(element2);
    }

    @Test
    public void givenTwoElementsWhenMultipliedThenSucceeds()
            throws GeneralCryptoLibException {
        BigInteger value1 = new BigInteger("3");
        BigInteger value2 = new BigInteger("4");
        BigInteger expectedResult = new BigInteger("12");

        multiplyAndAssert(value1, value2, expectedResult);
    }

    @Test
    public void givenAnElementWithValueOneWhenMultipliedWithASecondElementThenTheResultIsSecondElement()
            throws GeneralCryptoLibException {
        BigInteger value1 = new BigInteger("2");
        BigInteger value2 = BigInteger.ONE;
        BigInteger expectedResult = new BigInteger("2");

        multiplyAndAssert(value1, value2, expectedResult);
    }

    @Test
    public void givenTwoElementWhenMultipliedThenTheResultIsGreaterThanP()
            throws GeneralCryptoLibException {
        BigInteger value1 = new BigInteger("12");
        BigInteger value2 = new BigInteger("13");
        BigInteger expectedResult = new BigInteger("18");

        multiplyAndAssert(value1, value2, expectedResult);
    }

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

        BigInteger value1 = new BigInteger("3");
        ZpGroupElement element = new ZpGroupElement(value1, _group_g2_q11);

        element.exponentiate(null);
    }

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

        BigInteger value1 = new BigInteger("3");
        ZpGroupElement element = new ZpGroupElement(value1, _group_g2_q11);

        new MockUp<Exponent>() {
            @Mock
            public BigInteger getValue() {
                return null;
            }
        };
        BigInteger exponentValue = new BigInteger("3");
        Exponent exponent =
            new Exponent(_group_g2_q11.getQ(), exponentValue);

        element.exponentiate(exponent);
    }

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

        BigInteger value1 = new BigInteger("3");
        ZpGroupElement element = new ZpGroupElement(value1, _group_g2_q11);

        ZpSubgroup exponentGroup =
            new ZpSubgroup(_g, new BigInteger("7"), new BigInteger("3"));
        BigInteger exponentValue = new BigInteger("3");
        Exponent exponent =
            new Exponent(exponentGroup.getQ(), exponentValue);

        element.exponentiate(exponent);
    }

    @Test
    public void givenAnExponentWithValueZeroWhenExponentiateWithItThenResultIsOne()
            throws GeneralCryptoLibException {
        BigInteger value = new BigInteger("16");
        BigInteger exponentValue = BigInteger.ZERO;
        BigInteger expectedResult = BigInteger.ONE;

        exponentiateAndAssert(value, exponentValue, expectedResult);
    }

    @Test
    public void givenElementAndExponentWhenExponentiateThenSucceeds()
            throws GeneralCryptoLibException {
        BigInteger value = new BigInteger("2");
        BigInteger exponentValue = new BigInteger("4");
        BigInteger expectedResult = new BigInteger("16");

        exponentiateAndAssert(value, exponentValue, expectedResult);
    }

    @Test
    public void givenElementAndExponentWhenExponentiationThenResultGreaterThanQ()
            throws GeneralCryptoLibException {
        BigInteger value = new BigInteger("13");
        BigInteger exponentValue = new BigInteger("5");
        BigInteger expectedResult = new BigInteger("4");

        exponentiateAndAssert(value, exponentValue, expectedResult);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testExponentiateWithANullElement() throws Exception {
        ZpGroupElement element =
            new ZpGroupElement(BigInteger.TEN, _group_g2_q11);

        element.exponentiate(null);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testExponentiateWithAnElementWithNullValue(
            @Injectable final Exponent exponent)
            throws GeneralCryptoLibException {

        ZpGroupElement element =
            new ZpGroupElement(BigInteger.TEN, _group_g2_q11);

        new NonStrictExpectations() {
            {
                exponent.getValue();
                result = null;
            }
        };

        element.exponentiate(null);
    }

    @Test
    public void testEquals() throws GeneralCryptoLibException {

        ZpGroupElement element1_value1_q11 =
            new ZpGroupElement(BigInteger.ONE, _group_g2_q11);
        ZpGroupElement element2_value1_q11 =
            new ZpGroupElement(BigInteger.ONE, _group_g2_q11);

        ZpGroupElement element3_value2_q11 =
            new ZpGroupElement(new BigInteger("2"), _group_g2_q11);

        ZpSubgroup otherGroup_g4_q3 = new ZpSubgroup(new BigInteger("2"),
            new BigInteger("7"), new BigInteger("3"));
        ZpGroupElement element4_value1_q13 =
            new ZpGroupElement(BigInteger.ONE, otherGroup_g4_q3);

        new EqualsTester()
            .addEqualityGroup(element1_value1_q11, element2_value1_q11)
            .addEqualityGroup(element3_value2_q11)
            .addEqualityGroup(element4_value1_q13).testEquals();
    }

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

        ZpGroupElement element =
            new ZpGroupElement(new BigInteger("2"), _group_g2_q11);

        String jsonStr = element.toJson();

        ZpGroupElement reconstructedElement =
            ZpGroupElement.fromJson(jsonStr);

        String errorMessage =
            "The reconstructed Zp group element does not equal the original element.";
        Assert.assertEquals(errorMessage, reconstructedElement, element);
    }
    
    @Test
    public void testToString() throws GeneralCryptoLibException {
        ZpGroupElement element =
                new ZpGroupElement(new BigInteger("2"), _group_g2_q11);
        String toString = element.toString();
        Assert.assertTrue(toString.contains("=2,"));
        Assert.assertTrue(toString.contains("="+_group_g2_q11.getP().toString()));
        Assert.assertTrue(toString.contains("="+_group_g2_q11.getQ().toString()));
    }

    @Test
    public final void givenNullZpSubgroupPParamThenJsonDeserializationThrowsException()
            throws GeneralCryptoLibException {

        String jsonStr =
            "{\"zpGroupElement\":{\"q\":\"Cw==\",\"p\":null,\"value\":\"Ag==\"}}";

        thrown.expect(GeneralCryptoLibException.class);
        thrown.expectMessage("Zp subgroup p parameter is null.");

        ZpGroupElement.fromJson(jsonStr);
    }

    @Test
    public final void givenNullZpSubgroupQParamThenJsonDeserializationThrowsException()
            throws GeneralCryptoLibException {

        String jsonStr =
            "{\"zpGroupElement\":{\"q\":null,\"p\":\"Fw==\",\"value\":\"Ag==\"}}";

        thrown.expect(GeneralCryptoLibException.class);
        thrown.expectMessage("Zp subgroup q parameter is null.");

        ZpGroupElement.fromJson(jsonStr);
    }

    /**
     * Exponentiates an element by an exponent and asserts the expected result.
     *
     * @param elementValue
     *            The group element value to set.
     * @param exponentValue
     *            The exponent value to set.
     * @param expectedResult
     *            The expected result of the exponentiation.
     * @throws GeneralCryptoLibException
     */
    private void exponentiateAndAssert(final BigInteger elementValue,
            final BigInteger exponentValue,
            final BigInteger expectedResult)
            throws GeneralCryptoLibException {

        ZpGroupElement element =
            new ZpGroupElement(elementValue, _group_g2_q11);

        Exponent exponent =
            new Exponent(_group_g2_q11.getQ(), exponentValue);

        ZpGroupElement result = element.exponentiate(exponent);

        Assert.assertEquals(
            "The result of the exponentiation is not the expected.",
            expectedResult, result.getValue());
    }

    /**
     * Multiply two group elements with the values {@code value1} and
     * {@code value2}. Then asserts that the result has the value
     * {@code expectedResult}.
     *
     * @param value1
     *            First element to multiply.
     * @param value2
     *            Second element to multiply.
     * @param expectedResult
     *            The expected result of the {@code value1 * value2}.
     * @throws GeneralCryptoLibException
     */
    private void multiplyAndAssert(final BigInteger value1,
            final BigInteger value2, final BigInteger expectedResult)
            throws GeneralCryptoLibException {
        ZpGroupElement element1 =
            new ZpGroupElement(value1, _group_g2_q11);
        ZpGroupElement element2 =
            new ZpGroupElement(value2, _group_g2_q11);

        ZpGroupElement result = element1.multiply(element2);

        Assert.assertEquals(
            "The multiplication result is not the expected one",
            expectedResult, result.getValue());
    }

    /**
     * Inverts the element with the value {@code elementValue}, and checks
     * whether the result is the {@code expectedInverseValue}.
     *
     * @param elementValue
     *            The value of the element to invert.
     * @param expectedInverseValue
     *            The expected result of the invert operation of the element
     *            with value {@code elementValue}.
     * @throws GeneralCryptoLibException
     */
    private void invertAndAssert(final BigInteger elementValue,
            final BigInteger expectedInverseValue)
            throws GeneralCryptoLibException {
        ZpGroupElement element =
            new ZpGroupElement(elementValue, _group_g2_q11);

        ZpGroupElement inverse = element.invert();

        Assert.assertEquals("The returned element is not the inverse",
            expectedInverseValue, inverse.getValue());
    }
}
