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

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import org.junit.BeforeClass;
import org.junit.Test;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;

/**
 * Tests of ElGamalPublicKey.
 */
public class ElGamalPublicKeyTest {

    private static BigInteger _p;

    private static BigInteger _q;

    private static BigInteger _g;

    private static ZpSubgroup _smallGroup;

    private static int _numKeys;

    private static List<ZpGroupElement> _pubKeys;

    private static ElGamalPublicKey _elGamalPublicKey;

    @BeforeClass
    public static void setUp() throws GeneralCryptoLibException {

        _p = new BigInteger("23");

        _q = new BigInteger("11");

        _g = new BigInteger("2");

        _smallGroup = new ZpSubgroup(_g, _p, _q);

        _numKeys = 2;

        _pubKeys = new ArrayList<ZpGroupElement>();
        _pubKeys.add(new ZpGroupElement(_g, _smallGroup));
        _pubKeys.add(new ZpGroupElement(_g, _smallGroup));

        _elGamalPublicKey = new ElGamalPublicKey(_pubKeys, _smallGroup);
    }

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

        List<ZpGroupElement> nullKeysList = null;
        new ElGamalPublicKey(nullKeysList, _smallGroup);
    }

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

        List<ZpGroupElement> emptyKeysList =
            new ArrayList<ZpGroupElement>();
        new ElGamalPublicKey(emptyKeysList, _smallGroup);
    }

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

        ZpSubgroup nullGroup = null;
        new ElGamalPublicKey(_pubKeys, nullGroup);
    }

    @Test
    public void givenPublicKeyWhenGetKeysThenOK() {

        List<ZpGroupElement> returnedKeys = _elGamalPublicKey.getKeys();

        String errorMsg =
            "The created public key does not have the expected number of elements";
        assertEquals(errorMsg, _numKeys, returnedKeys.size());

        errorMsg = "The public does not have the expected list of keys";
        assertEquals(errorMsg, _pubKeys, returnedKeys);
    }

    @Test
    public void givenPublicKeyWhenGetGroupThenOK() {

        String errorMsg =
            "The created public key does not have the expected group";
        assertEquals(errorMsg, _smallGroup, _elGamalPublicKey.getGroup());
    }

    @Test
    public void givenJsonStringWhenReconstructThenEqualToOriginalPublicKey()
            throws GeneralCryptoLibException {

        BigInteger p = new BigInteger("23");
        BigInteger q = new BigInteger("11");
        BigInteger g = new BigInteger("2");
        ZpSubgroup smallGroup = new ZpSubgroup(g, p, q);

        List<ZpGroupElement> pubKeys = new ArrayList<ZpGroupElement>();
        pubKeys.add(new ZpGroupElement(g, smallGroup));
        pubKeys.add(new ZpGroupElement(g, smallGroup));
        ElGamalPublicKey expectedElGamalPublicKey =
            new ElGamalPublicKey(pubKeys, smallGroup);

        String jsonStr = _elGamalPublicKey.toJson();

        ElGamalPublicKey reconstructedPublicKey =
            ElGamalPublicKey.fromJson(jsonStr);

        String errorMsg =
            "The reconstructed ElGamal public key is not equal to the expected key";
        assertEquals(errorMsg, expectedElGamalPublicKey,
            reconstructedPublicKey);
    }

    @Test
    public void givenJsonStringWhenReconstructKeyThenDifferentFromOtherPublicKey()
            throws GeneralCryptoLibException {

        BigInteger p = new BigInteger("7");
        BigInteger q = new BigInteger("3");
        BigInteger g = new BigInteger("2");
        ZpSubgroup smallGroup = new ZpSubgroup(g, p, q);
        List<ZpGroupElement> pubKeys = new ArrayList<ZpGroupElement>();
        pubKeys.add(new ZpGroupElement(g, smallGroup));
        pubKeys.add(new ZpGroupElement(g, smallGroup));
        ElGamalPublicKey expectedElGamalPublicKey =
            new ElGamalPublicKey(pubKeys, smallGroup);

        String jsonStr = _elGamalPublicKey.toJson();

        ElGamalPublicKey reconstructedPublicKey =
            ElGamalPublicKey.fromJson(jsonStr);

        String errorMsg =
            "The reconstructed ElGamal public key was unexpectedly equal to";
        assertTrue(errorMsg,
            !expectedElGamalPublicKey.equals(reconstructedPublicKey));
    }

    @Test
    public void givenJsonStringWhenReconstructThenEqualToOriginalPublicKeySmall()
            throws GeneralCryptoLibException {

        BigInteger p = new BigInteger("23");
        BigInteger q = new BigInteger("11");
        BigInteger g = new BigInteger("2");
        ZpSubgroup groupSmall = new ZpSubgroup(g, p, q);

        List<ZpGroupElement> pubKeys = new ArrayList<ZpGroupElement>();
        BigInteger k1 = new BigInteger("8");
        BigInteger k2 = new BigInteger("4");
        BigInteger k3 = new BigInteger("18");
        pubKeys.add(new ZpGroupElement(k1, groupSmall));
        pubKeys.add(new ZpGroupElement(k2, groupSmall));
        pubKeys.add(new ZpGroupElement(k3, groupSmall));
        ElGamalPublicKey expectedElGamalPublicKey =
            new ElGamalPublicKey(pubKeys, groupSmall);

        String jsonStr = expectedElGamalPublicKey.toJson();

        ElGamalPublicKey reconstructedPublicKey =
            ElGamalPublicKey.fromJson(jsonStr);

        String errorMsg =
            "The reconstructed ElGamal public key is not equal to the expected key";
        assertEquals(errorMsg, expectedElGamalPublicKey,
            reconstructedPublicKey);
    }

    @Test
    public void givenJsonStringWhenReconstructThenEqualToOriginalPublicKeyLarge()
            throws GeneralCryptoLibException {

        BigInteger p = new BigInteger(
            "25878792566670842099842137716422866466252991028815773139028451679515364679624923581358662655689289205766441980239548823737806954397019411202244121935752456749381769565031670387914863935577896116425654849306598185507995737892509839616944496073707445338806101425467388977937489020456783676102620561970644684015868766028080049372849872115052208214439472603355483095640041515460851475971118272125133224007949688443680429668091313474118875081620746919907567682398209044343652147328622866834600839878114285018818463110227111614032671442085465843940709084719667865761125514800243342061732684028802646193202210299179139410607");
        BigInteger q = new BigInteger(
            "12939396283335421049921068858211433233126495514407886569514225839757682339812461790679331327844644602883220990119774411868903477198509705601122060967876228374690884782515835193957431967788948058212827424653299092753997868946254919808472248036853722669403050712733694488968744510228391838051310280985322342007934383014040024686424936057526104107219736301677741547820020757730425737985559136062566612003974844221840214834045656737059437540810373459953783841199104522171826073664311433417300419939057142509409231555113555807016335721042732921970354542359833932880562757400121671030866342014401323096601105149589569705303");
        BigInteger g = new BigInteger(
            "23337993065784550228812110720552652305178266477392633588884900695706615523553977368516877521940228584865573144621632575456086035440118913707895716109366641541746808409917179478292952139273396531060021729985473121368590574110220870149822495151519706210399569901298027813383104891697930149341258267962490850297875794622068418425473578455187344232698462829084010585324877420343904740081787639502967515631687068869545665294697583750184911025514712871193837246483893950501015755683415509019863976071649325968623617568219864744389709563087949389080252971419711636380986100047871404548371112472694814597772988558887480308242");
        ZpSubgroup groupLarge = new ZpSubgroup(g, p, q);

        List<ZpGroupElement> pubKeys = new ArrayList<ZpGroupElement>();
        BigInteger k1 = new BigInteger(
            "10813600900774814195443606333960059840422668831651681762833741452209135730650864452130214650095752196571587702814444040062441118243327189166429884552159103387262050740332278254634882734924804644475224947872860032029859548750165768253226371255576644038692950180048665862716256869418687777905840129397057458233193867066348913299433809742746548665012143326171826247072111249488645241753486115635552758795831876906471529661551590917603438603247246774800973230633192834223627007745551660750221242611378094020803537796039757375005102923882805858377750794407562467092342593013192207441450132449006118950054688176587133961409");
        BigInteger k2 = new BigInteger(
            "9853567379170719777334566185703517962033907422082302290399539440945952704005195929747815492644386838518442489536168205168599971300023125537903954233589099450715318090993081445448000242320602780219383745672665060815409904594899636902384560406070750905212550608478584637229051043378424454988191292610492156611060987968379597746425215082609688334075995471885941390029962061830957304051378886661637882858376301662242976957205952138813983434210148145737465280463387503441953147312554462853944227422544694220254912841114943534241459615181877522308107316310818302531331583848802731174268791963178918947077447910490965216158");
        BigInteger k3 = new BigInteger(
            "15794379152029066838713804099734041126263932884995939213200727618402701551640677264676158281588744352993384547095094861783499389509798314014022069284171927996645400863301983568835178765405753107568243261485121885250020254379553972077632932375247364025813645736274557285578258334201060356153010774984159753076425595005818465482275931002143043064230447825744365372619735966376047086115776775312766029914867090765966581873109229928865155261673639010273082385667041760650533946369556580260795606883221254081319827267155147350340767060491743854037592935823565514546490962430394933911057965977363230092438984010824700325167");
        pubKeys.add(new ZpGroupElement(k1, groupLarge));
        pubKeys.add(new ZpGroupElement(k2, groupLarge));
        pubKeys.add(new ZpGroupElement(k3, groupLarge));
        ElGamalPublicKey expectedElGamalPublicKey =
            new ElGamalPublicKey(pubKeys, groupLarge);

        String jsonStr = expectedElGamalPublicKey.toJson();

        ElGamalPublicKey reconstructedPublicKey =
            ElGamalPublicKey.fromJson(jsonStr);

        String errorMsg =
            "The reconstructed ElGamal public key is not equal to the expected key";
        assertEquals(errorMsg, expectedElGamalPublicKey,
            reconstructedPublicKey);
    }

    @Test
    public void givenKeyWhenMultiplyThenOK()
            throws GeneralCryptoLibException {
        List<ZpGroupElement> elements =
            asList(new ZpGroupElement(BigInteger.valueOf(3), _smallGroup));
        ElGamalPublicKey other =
            new ElGamalPublicKey(elements, _smallGroup);
        ElGamalPublicKey product = _elGamalPublicKey.multiply(other);
        assertEquals(_smallGroup, product.getGroup());
        List<ZpGroupElement> productElements = product.getKeys();
        assertEquals(1, productElements.size());
        assertEquals(
            new ZpGroupElement(BigInteger.valueOf(6), _smallGroup),
            productElements.get(0));
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenNullWhenMultiplyThenException()
            throws GeneralCryptoLibException {
        _elGamalPublicKey.multiply((ElGamalPublicKey) null);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenKeyWithDifferentGroupWhenMultiplyThenException()
            throws GeneralCryptoLibException {
        ZpSubgroup group = new ZpSubgroup(BigInteger.valueOf(2),
            BigInteger.valueOf(7), BigInteger.valueOf(3));
        List<ZpGroupElement> elements =
            asList(new ZpGroupElement(BigInteger.valueOf(3), group));
        ElGamalPublicKey other = new ElGamalPublicKey(elements, group);
        _elGamalPublicKey.multiply(other);
    }

    @Test
    public void givenArrayOfKeysWhenMultiplyThenOK()
            throws GeneralCryptoLibException {
        List<ZpGroupElement> elements =
            asList(new ZpGroupElement(BigInteger.valueOf(3), _smallGroup));
        ElGamalPublicKey first =
            new ElGamalPublicKey(elements, _smallGroup);
        elements =
            asList(new ZpGroupElement(BigInteger.valueOf(4), _smallGroup));
        ElGamalPublicKey other =
            new ElGamalPublicKey(elements, _smallGroup);
        ElGamalPublicKey product =
            ElGamalPublicKey.multiply(_elGamalPublicKey, first, other);
        assertEquals(_smallGroup, product.getGroup());
        List<ZpGroupElement> productElements = product.getKeys();
        assertEquals(1, productElements.size());
        assertEquals(new ZpGroupElement(BigInteger.ONE, _smallGroup),
            productElements.get(0));
    }

    @Test
    public void givenCollectionOfKeysWhenCombineThenOK()
            throws GeneralCryptoLibException {
        List<ZpGroupElement> elements =
            asList(new ZpGroupElement(BigInteger.valueOf(3), _smallGroup));
        ElGamalPublicKey other =
            new ElGamalPublicKey(elements, _smallGroup);
        ElGamalPublicKey product =
            ElGamalPublicKey.multiply(asList(_elGamalPublicKey, other));
        assertEquals(_smallGroup, product.getGroup());
        List<ZpGroupElement> productElements = product.getKeys();
        assertEquals(1, productElements.size());
        assertEquals(
            new ZpGroupElement(BigInteger.valueOf(6), _smallGroup),
            productElements.get(0));
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenCollectionOfKeysWithNullWhenCombineThenException()
            throws GeneralCryptoLibException {
        ElGamalPublicKey.multiply(asList(_elGamalPublicKey, null));
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenCollectionOfKeysWithDifferentGroupWhenCombineThenException()
            throws GeneralCryptoLibException {
        ZpSubgroup group = new ZpSubgroup(BigInteger.valueOf(2),
            BigInteger.valueOf(7), BigInteger.valueOf(3));
        List<ZpGroupElement> elements =
            asList(new ZpGroupElement(BigInteger.valueOf(3), group));
        ElGamalPublicKey other = new ElGamalPublicKey(elements, group);
        ElGamalPublicKey.multiply(asList(_elGamalPublicKey, other));
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void givenEmptyCollectionOfKeysWhenCombineThenException()
            throws GeneralCryptoLibException {
        ElGamalPublicKey.multiply(emptyList());
    }

    @Test
    public void testBinarySerialization()
            throws GeneralCryptoLibException {
        byte[] bytes = _elGamalPublicKey.toBytes();
        ElGamalPublicKey other = ElGamalPublicKey.fromBytes(bytes);
        assertEquals(other, _elGamalPublicKey);
    }

    @Test
    public void testInvert() throws GeneralCryptoLibException {
        ElGamalPublicKey inverted = _elGamalPublicKey.invert();
        assertEquals(_smallGroup, inverted.getGroup());
        List<ZpGroupElement> invertedElements = inverted.getKeys();
        assertEquals(2, invertedElements.size());
        assertEquals(
            new ZpGroupElement(BigInteger.valueOf(12), _smallGroup),
            invertedElements.get(0));
        assertEquals(
            new ZpGroupElement(BigInteger.valueOf(12), _smallGroup),
            invertedElements.get(1));
    }

    @Test
    public void testDivide() throws GeneralCryptoLibException {
        List<ZpGroupElement> elements =
            asList(new ZpGroupElement(BigInteger.valueOf(6), _smallGroup));
        ElGamalPublicKey other =
            new ElGamalPublicKey(elements, _smallGroup);
        ElGamalPublicKey quotient = _elGamalPublicKey.divide(other);
        assertEquals(_smallGroup, quotient.getGroup());
        List<ZpGroupElement> quotientElements = quotient.getKeys();
        assertEquals(1, quotientElements.size());
        assertEquals(
            new ZpGroupElement(BigInteger.valueOf(8), _smallGroup),
            quotientElements.get(0));
    }
}
