/*
 * 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.products.ov.vv.service;

import static org.hamcrest.core.Is.is;

import java.io.IOException;
import java.math.BigInteger;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.json.Json;
import javax.json.JsonObjectBuilder;

import org.apache.commons.codec.binary.Base64;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import org.slf4j.Logger;

import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
import com.scytl.products.ov.commons.beans.domain.model.confirmation.TraceableConfirmationMessage;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.validation.CryptographicOperationException;
import com.scytl.products.ov.commons.dto.CastCodeAndComputeResults;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.vv.domain.model.choicecode.CodesComputeResults;
import com.scytl.products.ov.vv.domain.model.content.CastCodeSignerCertRepository;
import com.scytl.products.ov.vv.domain.model.verification.Verification;
import com.scytl.products.ov.vv.domain.model.verification.VerificationRepository;
import com.scytl.products.ov.vv.domain.model.verificationset.VerificationSetEntity;
import com.scytl.products.ov.vv.domain.model.verificationset.VerificationSetRepository;

@RunWith(MockitoJUnitRunner.class)
public class CastCodesServiceImplTest {

    private final String TENANT_ID = "1";

    private final String ELECTION_EVENT_ID = "2";

    private final String VERIFICATION_CARD_ID = "3";

    private final String VERIFICATION_CARD_SET_ID = "5";

    private final String VOTE_CAST_CODE = Base64.encodeBase64String("789".getBytes());

    private final String VOTE_CAST_SIGNATURE_BASE64 = Base64.encodeBase64String("789".getBytes());

    @Mock
    private VerificationRepository verificationRepository;

    // The service that will be used to calculate the long codes
    @Mock
    private AsymmetricServiceAPI asymmetricService;

    // The service used to calculate long choice codes
    @Mock
    private LongCodesService longCodesService;

    // The service used to retrieve short choice codes
    @Mock
    private ShortCodesService shortCodesService;

    // The verification set repository that will give us the verification card
    // set for the voter verification card
    @Mock
    private VerificationSetRepository verificationSetRepository;

    // The certificate repository that will give us the vote cast code signing
    // certificates
    @Mock
    private CastCodeSignerCertRepository castCodeSignerCertRepository;

    @Mock
    private CodesComputeService codesComputeService;

    @Mock
    private Logger LOG;

    @Mock
    private TrackIdInstance trackIdInstance;

    @Mock
    private SecureLoggingWriter secureLoggerWriter;

    @InjectMocks
    CastCodeService sut = new CastCodesServiceImpl();

    @Test
    public void testRetrieveCastCodeAndSignature_Successful()
            throws ResourceNotFoundException, CryptographicOperationException, GeneralCryptoLibException,
            ClassNotFoundException, IOException {

        Verification verificationMock = new Verification();
        verificationMock.setVerificationCardSetId(VERIFICATION_CARD_SET_ID);

        X509Certificate certificateMock = Mockito.mock(X509Certificate.class);

        VerificationSetEntity verificationSetMock = new VerificationSetEntity();
        verificationSetMock.setJSON(prepareVerificationSetMockJSON());

        Mockito.when(verificationRepository.findByTenantIdElectionEventIdVerificationCardId(TENANT_ID,
            ELECTION_EVENT_ID, VERIFICATION_CARD_ID)).thenReturn(verificationMock);
        Mockito.when(castCodeSignerCertRepository.findByTenantEEIDVerificationCardSetId(TENANT_ID, ELECTION_EVENT_ID,
            VERIFICATION_CARD_SET_ID)).thenReturn(certificateMock);
        Mockito.when(verificationSetRepository.findByTenantIdElectionEventIdVerificationCardSetId(TENANT_ID,
            ELECTION_EVENT_ID, VERIFICATION_CARD_SET_ID)).thenReturn(verificationSetMock);
        Mockito.when(longCodesService.calculateLongCodes(Matchers.eq(ELECTION_EVENT_ID),
            Matchers.eq(VERIFICATION_CARD_ID), Matchers.any(), Matchers.any())).thenReturn(prepareLongCodeMock());
        Mockito.when(shortCodesService.retrieveShortCodes(Matchers.eq(TENANT_ID), Matchers.eq(ELECTION_EVENT_ID),
            Matchers.eq(VERIFICATION_CARD_ID), Matchers.any())).thenReturn(prepareShortCodeMock());
        Mockito.when(asymmetricService.verifySignature(Matchers.any(), Matchers.any(), Matchers.any(), Matchers.any()))
            .thenReturn(true);
        Map<BigInteger, ZpGroupElement> codes = new HashMap<>();
        codes.put(new BigInteger("789"),
            new ZpGroupElement(new BigInteger("788"), new BigInteger("789"), new BigInteger("1")));
        CodesComputeResults computeResults = new CodesComputeResults(codes, null);
        Mockito.when(codesComputeService.computePartialCodes(Matchers.any(), Matchers.any(), Matchers.any(),
            Matchers.any(), Matchers.any(), Matchers.any())).thenReturn(computeResults);

        TraceableConfirmationMessage castCodeMessage = new TraceableConfirmationMessage();
        castCodeMessage.setConfirmationKey(VOTE_CAST_CODE);
        castCodeMessage.setSignature(VOTE_CAST_SIGNATURE_BASE64);
        CastCodeAndComputeResults castCodeMessageResult =
            sut.retrieveCastCodeAndSignature(TENANT_ID, ELECTION_EVENT_ID, VERIFICATION_CARD_ID, castCodeMessage);

        Assert.assertThat(castCodeMessageResult.getVoteCastCode(), is(VOTE_CAST_CODE));
    }

    @Test(expected = ResourceNotFoundException.class)
    public void testRetrieveCastCodeAndSignature_LongCodes_ResourceNotFound()
            throws ResourceNotFoundException, CryptographicOperationException, GeneralCryptoLibException,
            ClassNotFoundException, IOException {

        Verification verificationMock = new Verification();
        verificationMock.setVerificationCardSetId(VERIFICATION_CARD_SET_ID);

        X509Certificate certificateMock = Mockito.mock(X509Certificate.class);

        VerificationSetEntity verificationSetMock = new VerificationSetEntity();
        verificationSetMock.setJSON(prepareVerificationSetMockJSON());

        Mockito.when(verificationRepository.findByTenantIdElectionEventIdVerificationCardId(TENANT_ID,
            ELECTION_EVENT_ID, VERIFICATION_CARD_ID)).thenReturn(verificationMock);
        Mockito.when(castCodeSignerCertRepository.findByTenantEEIDVerificationCardSetId(TENANT_ID, ELECTION_EVENT_ID,
            VERIFICATION_CARD_SET_ID)).thenReturn(certificateMock);
        Mockito.when(verificationSetRepository.findByTenantIdElectionEventIdVerificationCardSetId(TENANT_ID,
            ELECTION_EVENT_ID, VERIFICATION_CARD_SET_ID)).thenReturn(verificationSetMock);
        Map<BigInteger, ZpGroupElement> codes = new HashMap<>();
        codes.put(new BigInteger("789"),
            new ZpGroupElement(new BigInteger("788"), new BigInteger("789"), new BigInteger("1")));
        CodesComputeResults computeResults = new CodesComputeResults(codes, null);
        Mockito.when(codesComputeService.computePartialCodes(Matchers.any(), Matchers.any(), Matchers.any(),
            Matchers.any(), Matchers.any(), Matchers.any())).thenReturn(computeResults);
        Mockito
            .when(longCodesService.calculateLongCodes(Matchers.eq(ELECTION_EVENT_ID),
                Matchers.eq(VERIFICATION_CARD_ID), Matchers.any(), Matchers.any()))
            .thenThrow(ResourceNotFoundException.class);
        TraceableConfirmationMessage castCodeMessage = new TraceableConfirmationMessage();
        castCodeMessage.setConfirmationKey(VOTE_CAST_CODE);
        castCodeMessage.setSignature(VOTE_CAST_SIGNATURE_BASE64);
        sut.retrieveCastCodeAndSignature(TENANT_ID, ELECTION_EVENT_ID, VERIFICATION_CARD_ID, castCodeMessage);
    }

    @Test(expected = ResourceNotFoundException.class)
    public void testRetrieveCastCodeAndSignature_ShortCodesResourceNotFound()
            throws ResourceNotFoundException, CryptographicOperationException, GeneralCryptoLibException,
            ClassNotFoundException, IOException {

        Verification verificationMock = new Verification();
        verificationMock.setVerificationCardSetId(VERIFICATION_CARD_SET_ID);

        X509Certificate certificateMock = Mockito.mock(X509Certificate.class);

        VerificationSetEntity verificationSetMock = new VerificationSetEntity();
        verificationSetMock.setJSON(prepareVerificationSetMockJSON());

        Mockito.when(verificationRepository.findByTenantIdElectionEventIdVerificationCardId(TENANT_ID,
            ELECTION_EVENT_ID, VERIFICATION_CARD_ID)).thenReturn(verificationMock);
        Mockito.when(castCodeSignerCertRepository.findByTenantEEIDVerificationCardSetId(TENANT_ID, ELECTION_EVENT_ID,
            VERIFICATION_CARD_SET_ID)).thenReturn(certificateMock);
        Mockito.when(verificationSetRepository.findByTenantIdElectionEventIdVerificationCardSetId(TENANT_ID,
            ELECTION_EVENT_ID, VERIFICATION_CARD_SET_ID)).thenReturn(verificationSetMock);
        Mockito.when(longCodesService.calculateLongCodes(Matchers.eq(ELECTION_EVENT_ID),
            Matchers.eq(VERIFICATION_CARD_ID), Matchers.any(), Matchers.any())).thenReturn(prepareLongCodeMock());
        Map<BigInteger, ZpGroupElement> codes = new HashMap<>();
        codes.put(new BigInteger("789"),
            new ZpGroupElement(new BigInteger("788"), new BigInteger("789"), new BigInteger("1")));
        CodesComputeResults computeResults = new CodesComputeResults(codes, null);
        Mockito.when(codesComputeService.computePartialCodes(Matchers.any(), Matchers.any(), Matchers.any(),
            Matchers.any(), Matchers.any(), Matchers.any())).thenReturn(computeResults);
        Mockito.when(shortCodesService.retrieveShortCodes(Matchers.eq(TENANT_ID), Matchers.eq(ELECTION_EVENT_ID),
            Matchers.eq(VERIFICATION_CARD_ID), Matchers.any())).thenThrow(ResourceNotFoundException.class);
        TraceableConfirmationMessage castCodeMessage = new TraceableConfirmationMessage();
        castCodeMessage.setConfirmationKey(VOTE_CAST_CODE);
        castCodeMessage.setSignature(VOTE_CAST_SIGNATURE_BASE64);
        sut.retrieveCastCodeAndSignature(TENANT_ID, ELECTION_EVENT_ID, VERIFICATION_CARD_ID, castCodeMessage);
    }

    @Test(expected = CryptographicOperationException.class)
    public void testRetrieveCastCodeAndSignature_VerifiySignatureFail()
            throws ResourceNotFoundException, CryptographicOperationException, GeneralCryptoLibException,
            ClassNotFoundException, IOException {

        Verification verificationMock = new Verification();
        verificationMock.setVerificationCardSetId(VERIFICATION_CARD_SET_ID);

        X509Certificate certificateMock = Mockito.mock(X509Certificate.class);

        VerificationSetEntity verificationSetMock = new VerificationSetEntity();
        verificationSetMock.setJSON(prepareVerificationSetMockJSON());

        Mockito.when(verificationRepository.findByTenantIdElectionEventIdVerificationCardId(TENANT_ID,
            ELECTION_EVENT_ID, VERIFICATION_CARD_ID)).thenReturn(verificationMock);
        Mockito.when(castCodeSignerCertRepository.findByTenantEEIDVerificationCardSetId(TENANT_ID, ELECTION_EVENT_ID,
            VERIFICATION_CARD_SET_ID)).thenReturn(certificateMock);
        Mockito.when(verificationSetRepository.findByTenantIdElectionEventIdVerificationCardSetId(TENANT_ID,
            ELECTION_EVENT_ID, VERIFICATION_CARD_SET_ID)).thenReturn(verificationSetMock);
        Mockito.when(longCodesService.calculateLongCodes(Matchers.eq(ELECTION_EVENT_ID),
            Matchers.eq(VERIFICATION_CARD_ID), Matchers.any(), Matchers.any())).thenReturn(prepareLongCodeMock());
        Mockito.when(shortCodesService.retrieveShortCodes(Matchers.eq(TENANT_ID), Matchers.eq(ELECTION_EVENT_ID),
            Matchers.eq(VERIFICATION_CARD_ID), Matchers.any())).thenReturn(prepareShortCodeMock());
        Map<BigInteger, ZpGroupElement> codes = new HashMap<>();
        codes.put(new BigInteger("789"),
            new ZpGroupElement(new BigInteger("788"), new BigInteger("789"), new BigInteger("1")));
        CodesComputeResults computeResults = new CodesComputeResults(codes, null);
        Mockito.when(codesComputeService.computePartialCodes(Matchers.any(), Matchers.any(), Matchers.any(),
            Matchers.any(), Matchers.any(), Matchers.any())).thenReturn(computeResults);
        Mockito.when(asymmetricService.verifySignature(Matchers.any(), Matchers.any(), Matchers.any(), Matchers.any()))
            .thenReturn(false);
        TraceableConfirmationMessage castCodeMessage = new TraceableConfirmationMessage();
        castCodeMessage.setConfirmationKey(VOTE_CAST_CODE);
        castCodeMessage.setSignature(VOTE_CAST_SIGNATURE_BASE64);
        sut.retrieveCastCodeAndSignature(TENANT_ID, ELECTION_EVENT_ID, VERIFICATION_CARD_ID, castCodeMessage);
    }

    @Test(expected = CryptographicOperationException.class)
    public void testRetrieveCastCodeAndSignature_VerifiySignatureError()
            throws ResourceNotFoundException, CryptographicOperationException, GeneralCryptoLibException,
            ClassNotFoundException, IOException {

        Verification verificationMock = new Verification();
        verificationMock.setVerificationCardSetId(VERIFICATION_CARD_SET_ID);

        X509Certificate certificateMock = Mockito.mock(X509Certificate.class);

        VerificationSetEntity verificationSetMock = new VerificationSetEntity();
        verificationSetMock.setJSON(prepareVerificationSetMockJSON());

        Mockito.when(verificationRepository.findByTenantIdElectionEventIdVerificationCardId(TENANT_ID,
            ELECTION_EVENT_ID, VERIFICATION_CARD_ID)).thenReturn(verificationMock);
        Mockito.when(castCodeSignerCertRepository.findByTenantEEIDVerificationCardSetId(TENANT_ID, ELECTION_EVENT_ID,
            VERIFICATION_CARD_SET_ID)).thenReturn(certificateMock);
        Mockito.when(verificationSetRepository.findByTenantIdElectionEventIdVerificationCardSetId(TENANT_ID,
            ELECTION_EVENT_ID, VERIFICATION_CARD_SET_ID)).thenReturn(verificationSetMock);
        Mockito.when(longCodesService.calculateLongCodes(Matchers.eq(ELECTION_EVENT_ID),
            Matchers.eq(VERIFICATION_CARD_ID), Matchers.any(), Matchers.any())).thenReturn(prepareLongCodeMock());
        Mockito.when(shortCodesService.retrieveShortCodes(Matchers.eq(TENANT_ID), Matchers.eq(ELECTION_EVENT_ID),
            Matchers.eq(VERIFICATION_CARD_ID), Matchers.any())).thenReturn(prepareShortCodeMock());
        Map<BigInteger, ZpGroupElement> codes = new HashMap<>();
        codes.put(new BigInteger("789"),
            new ZpGroupElement(new BigInteger("788"), new BigInteger("789"), new BigInteger("1")));
        CodesComputeResults computeResults = new CodesComputeResults(codes, null);
        Mockito.when(codesComputeService.computePartialCodes(Matchers.any(), Matchers.any(), Matchers.any(),
            Matchers.any(), Matchers.any(), Matchers.any())).thenReturn(computeResults);
        Mockito.when(asymmetricService.verifySignature(Matchers.any(), Matchers.any(), Matchers.any(), Matchers.any()))
            .thenThrow(GeneralCryptoLibException.class);
        TraceableConfirmationMessage castCodeMessage = new TraceableConfirmationMessage();
        castCodeMessage.setConfirmationKey(VOTE_CAST_CODE);
        castCodeMessage.setSignature(VOTE_CAST_SIGNATURE_BASE64);
        sut.retrieveCastCodeAndSignature(TENANT_ID, ELECTION_EVENT_ID, VERIFICATION_CARD_ID, castCodeMessage);
    }

    private List<byte[]> prepareLongCodeMock() {
        return new ArrayList<>();
    }

    private List<String> prepareShortCodeMock() {
        List<String> shortCodes = new ArrayList<String>();
        shortCodes.add(VOTE_CAST_CODE + ";" + VOTE_CAST_SIGNATURE_BASE64);
        return shortCodes;
    }

    private String prepareVerificationSetMockJSON() throws GeneralCryptoLibException {
        List<ZpGroupElement> keyElements = new ArrayList<ZpGroupElement>();
        keyElements.add(new ZpGroupElement(new BigInteger("2"), new BigInteger("7"), new BigInteger("3")));
        ZpSubgroup zpSubgroup = new ZpSubgroup(new BigInteger("2"), new BigInteger("7"), new BigInteger("3"));
        String elGamalPkJson = new ElGamalPublicKey(keyElements, zpSubgroup).toJson();
        String elGamalPkJson_Base64 = Base64.encodeBase64String(elGamalPkJson.getBytes());

        JsonObjectBuilder jsonObjectBuilder = Json.createObjectBuilder();
        jsonObjectBuilder.add(CastCodesServiceImpl.PUBLIC_KEY, elGamalPkJson_Base64);

        return jsonObjectBuilder.build().toString();
    }

}
