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

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;

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

import org.junit.Before;
import org.junit.Test;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.proofs.ProofsServiceAPI;
import com.scytl.cryptolib.elgamal.bean.CiphertextImpl;
import com.scytl.cryptolib.elgamal.bean.ElGamalComputationsValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncrypterValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.bean.WitnessImpl;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.elgamal.cryptoapi.Witness;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.cryptolib.mathematical.groups.utils.MathematicalTestDataGenerator;
import com.scytl.cryptolib.proofs.bean.ProofPreComputedValues;
import com.scytl.cryptolib.proofs.cryptoapi.ProofPreComputerAPI;
import com.scytl.cryptolib.proofs.cryptoapi.ProofProverAPI;
import com.scytl.cryptolib.proofs.cryptoapi.ProofVerifierAPI;
import com.scytl.cryptolib.proofs.proof.Proof;

/**
 * Tests of OR proof validations.
 */
public class ORProofValidationTest {
    private ZpSubgroup group;

    private ProofPreComputerAPI preComputer;

    private ProofProverAPI prover;

    private ProofVerifierAPI verifier;

    private Ciphertext ciphertext;

    private ElGamalPublicKey publicKey;

    private Witness witness;

    private int index;

    private List<ZpGroupElement> elements;

    private String data;

    @Before
    public void setUp() throws GeneralCryptoLibException {
        group = MathematicalTestDataGenerator.getZpSubgroup();

        ProofsServiceAPI service = new ProofsService();
        preComputer = service.createProofPreComputerAPI(group);
        prover = service.createProofProverAPI(group);
        verifier = service.createProofVerifierAPI(group);

        Exponent privateKey =
            new Exponent(group.getQ(), BigInteger.valueOf(2));
        ZpGroupElement publicKeyComponent =
            group.getGenerator().exponentiate(privateKey);
        publicKey =
            new ElGamalPublicKey(singletonList(publicKeyComponent), group);

        ZpGroupElement element = group.getGenerator();
        elements = new ArrayList<>();
        elements.add(element);
        element = element.multiply(group.getGenerator());
        elements.add(element);
        element = element.multiply(group.getGenerator());
        elements.add(element);
        element = element.multiply(group.getGenerator());
        elements.add(element);
        element = element.multiply(group.getGenerator());
        elements.add(element);

        index = 3;

        Exponent exponent =
            new Exponent(group.getQ(), BigInteger.valueOf(3));

        ciphertext = new CiphertextImpl(
            group.getGenerator().exponentiate(exponent), publicKeyComponent
                .exponentiate(exponent).multiply(elements.get(index)));

        witness = new WitnessImpl(exponent);

        data = "data";
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testPreComputerWithInvalidElementCount()
            throws GeneralCryptoLibException {
        preComputer.preComputeORProof(publicKey, 0);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testPreComputerWithInvalidKey()
            throws GeneralCryptoLibException {
        group = new ZpSubgroup(BigInteger.valueOf(2),
            BigInteger.valueOf(23), BigInteger.valueOf(11));
        publicKey = new ElGamalPublicKey(publicKey.getKeys(), group);
        preComputer.preComputeORProof(publicKey, elements.size());
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testPreComputerWithNullKey()
            throws GeneralCryptoLibException {
        preComputer.preComputeORProof(null, elements.size());
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithEmptyElements()
            throws GeneralCryptoLibException {
        prover.createORProof(ciphertext, publicKey, witness, index,
            emptyList(), data);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithIndexOutOfBounds()
            throws GeneralCryptoLibException {
        prover.createORProof(ciphertext, publicKey, witness,
            elements.size(), elements, data);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithInvalidCiphertext()
            throws GeneralCryptoLibException {
        ciphertext = new CiphertextImpl(group.getGenerator(),
                group.getGenerator(), group.getGenerator());
        prover.createORProof(ciphertext, publicKey, witness, index,
            elements, data);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithInvalidElements()
            throws GeneralCryptoLibException {
        prover.createORProof(ciphertext, publicKey, witness, index,
            singletonList(null), data);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithInvalidKey()
            throws GeneralCryptoLibException {
        Exponent privateKey =
            new Exponent(group.getQ(), BigInteger.valueOf(2));
        ZpGroupElement publicKeyComponent =
            group.getGenerator().exponentiate(privateKey);
        publicKey = new ElGamalPublicKey(
            asList(publicKeyComponent, publicKeyComponent), group);
        prover.createORProof(ciphertext, publicKey, witness, index,
            elements, data);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithInvalidNumberOfPrecomputedExponents()
            throws GeneralCryptoLibException {
        List<Exponent> exponents =
            singletonList(new Exponent(group.getQ(), BigInteger.ONE));
        List<ZpGroupElement> phiOutputs =
            new ArrayList<>(elements.size() * 2);
        for (int i = 0; i < elements.size() * 2; i++) {
            phiOutputs.add(group.getGenerator());
        }
        ProofPreComputedValues values =
            new ProofPreComputedValues(exponents, phiOutputs);
        prover.createORProof(ciphertext, publicKey, null, index, elements,
            data, values);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithInvalidNumberOfPrecomputedPhis()
            throws GeneralCryptoLibException {
        List<Exponent> exponents = new ArrayList<>(elements.size());
        for (int i = 0; i < elements.size(); i++) {
            exponents.add(
                new Exponent(group.getQ(), BigInteger.valueOf(i + 1)));
        }
        List<ZpGroupElement> phiOutputs =
            singletonList(group.getGenerator());
        ProofPreComputedValues values =
            new ProofPreComputedValues(exponents, phiOutputs);
        prover.createORProof(ciphertext, publicKey, null, index, elements,
            data, values);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithInvalidWitness()
            throws GeneralCryptoLibException {
        Exponent exponent =
            new Exponent(group.getP(), BigInteger.valueOf(3));
        ElGamalComputationsValues values = new ElGamalComputationsValues(
            asList(group.getGenerator(), group.getGenerator()));
        witness = new ElGamalEncrypterValues(exponent, values);
        prover.createORProof(ciphertext, publicKey, witness, index,
            elements, data);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithNegativeIndex()
            throws GeneralCryptoLibException {
        prover.createORProof(ciphertext, publicKey, witness, -1, elements,
            data);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithNullCiphertext()
            throws GeneralCryptoLibException {
        prover.createORProof(null, publicKey, witness, index, elements,
            data);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithNullData() throws GeneralCryptoLibException {
        prover.createORProof(ciphertext, publicKey, witness, index,
            elements, null);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithNullElements()
            throws GeneralCryptoLibException {
        prover.createORProof(ciphertext, publicKey, witness, index, null,
            data);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithNullKey() throws GeneralCryptoLibException {
        prover.createORProof(ciphertext, null, witness, index, elements,
            data);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithNullPreComputedValues()
            throws GeneralCryptoLibException {
        prover.createORProof(ciphertext, publicKey, null, index, elements,
            data, null);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testProverWithNullWitness()
            throws GeneralCryptoLibException {
        prover.createORProof(ciphertext, publicKey, null, index, elements,
            data);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testVerifierWithEmptyElements()
            throws GeneralCryptoLibException {
        Proof proof = prover.createORProof(ciphertext, publicKey, witness,
            index, elements, data);
        verifier.verifyORProof(ciphertext, publicKey, emptyList(), data,
            proof);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testVerifierWithInvalidElements()
            throws GeneralCryptoLibException {
        Proof proof = prover.createORProof(ciphertext, publicKey, witness,
            index, elements, data);
        verifier.verifyORProof(ciphertext, publicKey, singletonList(null),
            data, proof);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testVerifierWithInvalidHashProof()
            throws GeneralCryptoLibException {
        Proof proof = prover.createORProof(ciphertext, publicKey, witness,
            index, elements, data);
        Exponent hash = new Exponent(group.getP(), BigInteger.valueOf(2));
        proof = new Proof(hash, proof.getValuesList());
        verifier.verifyORProof(ciphertext, publicKey, elements, data,
            proof);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testVerifierWithInvalidKey()
            throws GeneralCryptoLibException {
        Proof proof = prover.createORProof(ciphertext, publicKey, witness,
            index, elements, data);
        Exponent privateKey =
            new Exponent(group.getQ(), BigInteger.valueOf(2));
        ZpGroupElement publicKeyComnponent =
            group.getGenerator().exponentiate(privateKey);
        publicKey = new ElGamalPublicKey(
            asList(publicKeyComnponent, publicKeyComnponent), group);
        verifier.verifyORProof(ciphertext, publicKey, elements, data,
            proof);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testVerifierWithInvalidValuesProof()
            throws GeneralCryptoLibException {
        Proof proof = prover.createORProof(ciphertext, publicKey, witness,
            index, elements, data);
        List<Exponent> values = proof.getValuesList();
        values.remove(0);
        proof = new Proof(proof.getHashValue(), values);
        verifier.verifyORProof(ciphertext, publicKey, elements, data,
            proof);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testVerifierWithNullCiphertext()
            throws GeneralCryptoLibException {
        Proof proof = prover.createORProof(ciphertext, publicKey, witness,
            index, elements, data);
        verifier.verifyORProof(null, publicKey, elements, data, proof);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testVerifierWithNullData()
            throws GeneralCryptoLibException {
        Proof proof = prover.createORProof(ciphertext, publicKey, witness,
            index, elements, data);
        verifier.verifyORProof(ciphertext, publicKey, elements, null,
            proof);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testVerifierWithNullElements()
            throws GeneralCryptoLibException {
        Proof proof = prover.createORProof(ciphertext, publicKey, witness,
            index, elements, data);
        verifier.verifyORProof(ciphertext, publicKey, null, data, proof);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testVerifierWithNullKey()
            throws GeneralCryptoLibException {
        Proof proof = prover.createORProof(ciphertext, publicKey, witness,
            index, elements, data);
        verifier.verifyORProof(ciphertext, null, elements, data, proof);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testVerifierWithNullProof()
            throws GeneralCryptoLibException {
        verifier.verifyORProof(ciphertext, publicKey, elements, data,
            null);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testVerifierWithInvalidCiphertext()
            throws GeneralCryptoLibException {
        Proof proof = prover.createORProof(ciphertext, publicKey, witness,
            index, elements, data);
        ciphertext = new CiphertextImpl(group.getGenerator(),
            group.getGenerator(), group.getGenerator());
        verifier.verifyORProof(ciphertext, publicKey, elements, data,
            proof);
    }
}
