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

import static com.scytl.cryptolib.primitives.ffc.FFCData.G;
import static com.scytl.cryptolib.primitives.ffc.FFCData.L;
import static com.scytl.cryptolib.primitives.ffc.FFCData.N;
import static com.scytl.math.BigIntegers.modPow;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.math.BigInteger;

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

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.ffc.cryptoapi.FFCDomainParameters;
import com.scytl.cryptolib.ffc.cryptoapi.FFCPrime;
import com.scytl.cryptolib.ffc.cryptoapi.FFCPrimes;
import com.scytl.cryptolib.primitives.service.PrimitivesService;

/**
 * Tests of {@link FFCEngineImpl}.
 */
public class FFCEngineImplTest {
    private static final BigInteger FIRST_SEED = BigInteger.ONE;

    private static final FFCPrime P =
        new FFCPrime(FFCData.P, BigInteger.valueOf(2), 3);

    private static final FFCPrime Q =
        new FFCPrime(FFCData.Q, BigInteger.valueOf(4), 5);

    private static final FFCPrimes PRIMES =
        new FFCPrimes(P, Q, FIRST_SEED);

    private PseudoRandom random;

    private PseudoRandomFactory randomFactory;

    private FFCPrimesFactory primesFactory;

    private GFactory gFactory;

    private FFCEngineImpl engine;

    @Before
    public void setUp() throws GeneralCryptoLibException {
        random = mock(PseudoRandom.class);
        when(random.seed()).thenReturn(FIRST_SEED);

        randomFactory = mock(PseudoRandomFactory.class);
        when(randomFactory.newPseudoRandom(FIRST_SEED)).thenReturn(random);
        when(randomFactory.newPseudoRandom(anyInt())).thenReturn(random);

        primesFactory = mock(FFCPrimesFactory.class);
        when(primesFactory.newFFCPrimes(anyInt(), anyInt(), eq(random)))
            .thenReturn(PRIMES);

        gFactory = mock(GFactory.class);
        when(gFactory.newG(eq(PRIMES), anyInt())).thenReturn(G);

        engine = new FFCEngineImpl(randomFactory, primesFactory, gFactory);
    }

    @Test
    public void testGenerateDomainParameters()
            throws GeneralCryptoLibException {
        FFCDomainParameters parameters =
            engine.generateDomainParameters(L, N);
        assertEquals(PRIMES, parameters.primes());
        assertEquals(G, parameters.g());
        verify(randomFactory).newPseudoRandom(N);
        verify(primesFactory).newFFCPrimes(L, N, random);
        verify(gFactory).newG(PRIMES, 1);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testGenerateDomainParametersGFactoryFailed()
            throws GeneralCryptoLibException {
        when(gFactory.newG(PRIMES, 1))
            .thenThrow(new GeneralCryptoLibException("test"));
        engine.generateDomainParameters(L, N);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testGenerateDomainParametersInvalidL()
            throws GeneralCryptoLibException {
        engine.generateDomainParameters(L + 1, N);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testGenerateDomainParametersInvalidN()
            throws GeneralCryptoLibException {
        engine.generateDomainParameters(L, N + 1);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testGenerateDomainParametersPrimesFactoryFailed()
            throws GeneralCryptoLibException {
        when(primesFactory.newFFCPrimes(L, N, random))
            .thenThrow(new GeneralCryptoLibException("test"));
        engine.generateDomainParameters(L, N);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testGenerateDomainParametersRandomFactoryFailed()
            throws GeneralCryptoLibException {
        when(randomFactory.newPseudoRandom(anyInt()))
            .thenThrow(new GeneralCryptoLibException("test"));
        engine.generateDomainParameters(L, N);
    }

    @Test
    public void testGenerateDomainParametersValidLength()
            throws GeneralCryptoLibException {
        engine.generateDomainParameters(1024, 160);
        engine.generateDomainParameters(2048, 224);
        engine.generateDomainParameters(2048, 256);
        engine.generateDomainParameters(3072, 256);
    }

    @Test
    public void testGenerateG() throws GeneralCryptoLibException {
        assertEquals(G, engine.generateG(PRIMES, 2));
        verify(gFactory).newG(PRIMES, 2);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testGenerateGGFactoryFailed()
            throws GeneralCryptoLibException {
        when(gFactory.newG(PRIMES, 2))
            .thenThrow(new GeneralCryptoLibException("test"));
        engine.generateG(PRIMES, 2);
    }

    @Test
    public void testGeneratePrimes() throws GeneralCryptoLibException {
        assertEquals(PRIMES, engine.generatePrimes(L, N));
        verify(randomFactory).newPseudoRandom(N);
        verify(primesFactory).newFFCPrimes(L, N, random);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testGeneratePrimesInvalidL()
            throws GeneralCryptoLibException {
        engine.generateDomainParameters(L + 1, N);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testGeneratePrimesInvalidN()
            throws GeneralCryptoLibException {
        engine.generateDomainParameters(L, N + 1);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testGeneratePrimesRandomFactoryFailed()
            throws GeneralCryptoLibException {
        when(primesFactory.newFFCPrimes(L, N, random))
            .thenThrow(new GeneralCryptoLibException("test"));
        engine.generatePrimes(L, N);
    }

    @Test
    public void testGeneratePrimesValidLengths()
            throws GeneralCryptoLibException {
        engine.generatePrimes(1024, 160);
        engine.generatePrimes(2048, 224);
        engine.generatePrimes(2048, 256);
        engine.generatePrimes(3072, 256);
    }

    @Test
    public void testNewInstance() throws GeneralCryptoLibException {
        PrimitivesServiceAPI service = new PrimitivesService();
        FFCDomainParameters parameters =
            service.getFFCEngine().generateDomainParameters(L, N);

        BigInteger p = parameters.primes().p().value();
        BigInteger q = parameters.primes().q().value();
        BigInteger g = parameters.g();

        assertTrue(p.isProbablePrime(80));
        assertTrue(q.isProbablePrime(80));
        assertTrue(g.compareTo(p) < 0);
        assertEquals(BigInteger.ONE, modPow(g, q, p));
    }

    @Test
    public void testValidateDomainParameters()
            throws GeneralCryptoLibException {
        FFCDomainParameters parameters =
            new FFCDomainParameters(PRIMES, G);
        engine.validateDomainParameters(parameters);
        int l = P.value().bitLength();
        int n = Q.value().bitLength();
        verify(randomFactory).newPseudoRandom(PRIMES.firstSeed());
        verify(primesFactory).newFFCPrimes(l, n, random);
        verify(gFactory).newG(PRIMES, 1);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testValidateDomainParametersInvalid()
            throws GeneralCryptoLibException {
        FFCDomainParameters parameters =
            new FFCDomainParameters(PRIMES, G.add(BigInteger.ONE));
        engine.validateDomainParameters(parameters);
    }

    @Test
    public void testValidateG() throws GeneralCryptoLibException {
        engine.validateG(G, PRIMES, 2);
        verify(gFactory).newG(PRIMES, 2);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testValidateGInvalid() throws GeneralCryptoLibException {
        engine.validateG(G.add(BigInteger.ONE), PRIMES, 2);
    }

    @Test
    public void testValidatePrimes() throws GeneralCryptoLibException {
        engine.validatePrimes(PRIMES);
        int l = P.value().bitLength();
        int n = Q.value().bitLength();
        verify(randomFactory).newPseudoRandom(PRIMES.firstSeed());
        verify(primesFactory).newFFCPrimes(l, n, random);
    }

    @Test(expected = GeneralCryptoLibException.class)
    public void testValidatePrimesInvalid()
            throws GeneralCryptoLibException {
        engine.validatePrimes(new FFCPrimes(P, P, FIRST_SEED));
    }
}
