/*
 * 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.ea.services.infrastructure.persistence;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.scytl.products.ov.commons.test.CryptoUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Collections;
import java.util.Optional;

import javax.persistence.LockTimeoutException;
import javax.persistence.PessimisticLockException;

import org.apache.commons.io.FileUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.slf4j.Logger;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
import com.scytl.products.ov.commons.beans.domain.model.Information.VoterInformation;
import com.scytl.products.ov.commons.beans.domain.model.certificate.CertificateEntity;
import com.scytl.products.ov.commons.beans.exceptions.ApplicationException;
import com.scytl.products.ov.commons.beans.exceptions.DuplicateEntryException;
import com.scytl.products.ov.commons.beans.exceptions.EntryPersistenceException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.crypto.configuration.CertificateParameters;
import com.scytl.products.ov.commons.infrastructure.remote.client.RetrofitException;
import com.scytl.products.ov.commons.infrastructure.remote.service.RemoteCertificateService;
import com.scytl.products.ov.commons.infrastructure.transaction.TransactionContext;
import com.scytl.products.ov.commons.infrastructure.transaction.TransactionController;
import com.scytl.products.ov.commons.infrastructure.transaction.TransactionalAction;
import com.scytl.products.ov.commons.infrastructure.transaction.TransactionalActionException;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.verify.CSVVerifier;
import com.scytl.products.ov.ea.domain.model.EncryptedSVK;
import com.scytl.products.ov.ea.domain.model.extendedauthentication.AllowedAttemptsExceededException;
import com.scytl.products.ov.ea.domain.model.extendedauthentication.ExtendedAuthentication;
import com.scytl.products.ov.ea.domain.model.extendedauthentication.ExtendedAuthenticationRepository;
import com.scytl.products.ov.ea.services.infrastructure.remote.VoterMaterialService;

@RunWith(MockitoJUnitRunner.class)
@SuppressWarnings("unchecked")
public class ExtendedAuthenticationServiceImplTest extends ExtendedAuthenticationTest {

    public static final String COMMON_NAME = "certificate";

    private static final String VOTING_CARD_ID_2 = "26f4e529b51bca0f5f4fa6f1339a0245;Fk0vw+glXPNlTUnziNKH8w==";

    private static final String PDKDF_2 = "Fk0vw+glXPNlTUnziNKH8w==";

    private static final String SALT =
        "Mzk2OTY5NmI2YTZiNjczNjM0NjU3YTM4MzU2ZDczNzk2NjM4Nzk3NTc2MzM3Njc2NzA2MTczMzk2ZDZlNzMzNzM1NzE2ZTM0MzI3NDc5NjE2ZDc1NzMzMjZiMzgzNTc5NzYzNTc0NzQ3NDYyNjM3NTM2NmQ2YjY4NzM2NjY0Njg=";

    private static final String ENCRYPTED_START_VOTING_KEY =
        "QzZB//+c1Kn0WjppIsJhfQwuF4RUmdTaJf5YoG9iCaeMbW5en/eMxG+1W2NYkv0e";

    private static final String TENANT_ID = "1";

    private static final String ELECTION_EVENT = "2";

    private static final String EXTRA_PARAM = "e1ede18a8e1a4f7a8a65b6104dc0abed";

    public static final String INVALID = "invalid";

    @Mock
    private SecureLoggingWriter secureLoggerWriter;

    @Mock
    private TrackIdInstance trackIdInstance;

    @Mock
    private ExtendedAuthenticationRepository authenticationRepository;

    @Mock
    private Logger logger;

    @InjectMocks
    private ExtendedAuthenticationServiceImpl sut;

    @Mock
    private TransactionController transactionController;

    @Mock
    private RemoteCertificateService remoteCertificateService;

    @Mock
    private CSVVerifier verifier;

    @Mock
    private CertificateEntity certificateEntity;

    @Mock
    private VoterMaterialService voterMaterialService;

    @BeforeClass
    public static void setup() {
        ExtendedAuthenticationTest.setup();
        MockitoAnnotations.initMocks(ExtendedAuthenticationServiceImplTest.class);
    }

    @Before
    public void before() {
        this.setupSecureLogger();
        when(trackIdInstance.getTrackId()).thenReturn("trackId");
    }

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

    @Ignore
    @Test
    public void testSuccessfulAuthentication()
            throws ResourceNotFoundException, AllowedAttemptsExceededException, AuthenticationException,
            ApplicationException, GeneralCryptoLibException {
        ExtendedAuthentication extendedAuthentication =
            createEntity(TENANT_ID, PDKDF_2, ELECTION_EVENT, ENCRYPTED_START_VOTING_KEY, SALT, 0);
        when(authenticationRepository.getForUpdate(any(), any(), any()))
            .thenReturn(Optional.of(extendedAuthentication));

        EncryptedSVK authenticate = sut.authenticate(TENANT_ID, VOTING_CARD_ID_2, EXTRA_PARAM, ELECTION_EVENT);

        Assert.assertEquals(ENCRYPTED_START_VOTING_KEY, authenticate.getEncryptedSVK());
    }

    @Test
    public void testTooManyAttempts()
            throws ResourceNotFoundException, AllowedAttemptsExceededException, AuthenticationException,
            ApplicationException, GeneralCryptoLibException {
        expectedException.expect(AllowedAttemptsExceededException.class);

        ExtendedAuthentication extendedAuthentication =
            createEntity(TENANT_ID, PDKDF_2, ELECTION_EVENT, ENCRYPTED_START_VOTING_KEY, SALT, 6);
        when(authenticationRepository.getForUpdate(any(), any(), any()))
            .thenReturn(Optional.of(extendedAuthentication));

        sut.authenticate(TENANT_ID, VOTING_CARD_ID_2, EXTRA_PARAM, ELECTION_EVENT);
    }

    @Test(expected = ApplicationException.class)
    public void testAuthenticationLockTimeoutException()
            throws ResourceNotFoundException, AllowedAttemptsExceededException, AuthenticationException,
            ApplicationException, GeneralCryptoLibException {
        createEntity(TENANT_ID, PDKDF_2, ELECTION_EVENT, ENCRYPTED_START_VOTING_KEY, SALT, 0);
        when(authenticationRepository.getForUpdate(any(), any(), any()))
            .thenThrow(new LockTimeoutException("exception"));

        sut.authenticate(TENANT_ID, VOTING_CARD_ID_2, EXTRA_PARAM, ELECTION_EVENT);
    }

    @Test(expected = ApplicationException.class)
    public void testAuthenticationPessimisticLockException()
            throws ResourceNotFoundException, AllowedAttemptsExceededException, AuthenticationException,
            ApplicationException, GeneralCryptoLibException {
        createEntity(TENANT_ID, PDKDF_2, ELECTION_EVENT, ENCRYPTED_START_VOTING_KEY, SALT, 0);
        when(authenticationRepository.getForUpdate(any(), any(), any()))
            .thenThrow(new PessimisticLockException("exception"));

        sut.authenticate(TENANT_ID, VOTING_CARD_ID_2, EXTRA_PARAM, ELECTION_EVENT);
    }

    @Test
    public void testInvalidExtraParamAuthentication()
            throws ResourceNotFoundException, AllowedAttemptsExceededException, ApplicationException,
            AuthenticationException, GeneralCryptoLibException {

        expectedException.expect(AuthenticationException.class);
        expectedException.expectMessage("invalid extra parameter");

        ExtendedAuthentication extendedAuthentication =
            createEntity(TENANT_ID, PDKDF_2, ELECTION_EVENT, ENCRYPTED_START_VOTING_KEY, SALT, 0);
        when(authenticationRepository.getForUpdate(any(), any(), any()))
            .thenReturn(Optional.of(extendedAuthentication));
        when(trackIdInstance.getTrackId()).thenReturn("");
        sut.authenticate(TENANT_ID, VOTING_CARD_ID_2, "invalid", ELECTION_EVENT);

    }

    @Test
    public void testSaveExtendedAuthenticationInfoExisting()
            throws IOException, TransactionalActionException, ApplicationException, GeneralCryptoLibException,
            RetrofitException {
        final Path tempFile = Files.createTempFile("test.csv", null);

        FileUtils.writeStringToFile(tempFile.toFile(), "1,2,4,5,6" + System.lineSeparator() + "1,2,3,4,5");

        when(transactionController.doInNewTransaction(any(TransactionalAction.class))).then(defaultAnswer());
        when(authenticationRepository.getForUpdate(any(), any(), any()))
            .thenReturn(Optional.of(new ExtendedAuthentication()));

        mockCertificate();
        sut.saveExtendedAuthenticationFromFile(tempFile, TENANT_ID, ELECTION_EVENT, "");

    }

    @Test
    public void testSaveExtendedAuthenticationInfoNotExisting()
            throws IOException, TransactionalActionException, ApplicationException, GeneralCryptoLibException,
            RetrofitException {
        final Path tempFile = Files.createTempFile("test.csv", null);
        FileUtils.writeStringToFile(tempFile.toFile(), "1,2,4,5,6" + System.lineSeparator() + "1,2,3,4,5");

        when(transactionController.doInNewTransaction(any(TransactionalAction.class))).then(defaultAnswer());
        when(authenticationRepository.getForUpdate(any(), any(), any())).thenReturn(Optional.empty());

        mockCertificate();
        sut.saveExtendedAuthenticationFromFile(tempFile, TENANT_ID, ELECTION_EVENT, "");
    }

    @Test
    public void testSaveExtendedAuthenticationInfoTransactionException()
            throws IOException, TransactionalActionException, ApplicationException, GeneralCryptoLibException,
            RetrofitException {
        final Path tempFile = Files.createTempFile("test.csv", null);
        FileUtils.writeStringToFile(tempFile.toFile(), "1,2,4,5,6" + System.lineSeparator() + "1,2,3,4,5");

        when(transactionController.doInNewTransaction(any(TransactionalAction.class)))
            .thenThrow(new TransactionalActionException(new RuntimeException()));
        when(authenticationRepository.getForUpdate(any(), any(), any())).thenReturn(Optional.empty());

        mockCertificate();
        sut.saveExtendedAuthenticationFromFile(tempFile, TENANT_ID, ELECTION_EVENT, "");
    }

    @Test
    public void testUpdateExistingAuthenticationInfoSuccess() throws ApplicationException, ResourceNotFoundException {
        when(authenticationRepository.getForRead(any(), any(), any()))
            .thenReturn(Optional.of(new ExtendedAuthentication()));
        when(authenticationRepository.getForUpdate(any(), any(), any()))
            .thenReturn(Optional.of(new ExtendedAuthentication()));
        sut.renewExistingExtendedAuthentication(TENANT_ID, "", "", "", ELECTION_EVENT);
    }

    @Test(expected = ApplicationException.class)
    public void testUpdateExistingAuthenticationInfoDuplicate()
            throws ApplicationException, ResourceNotFoundException, DuplicateEntryException {
        when(authenticationRepository.getForRead(any(), any(), any()))
            .thenReturn(Optional.of(new ExtendedAuthentication()));
        when(authenticationRepository.getForUpdate(any(), any(), any()))
            .thenReturn(Optional.of(new ExtendedAuthentication()));
        when(authenticationRepository.save(any())).thenThrow(new DuplicateEntryException("exception"));
        sut.renewExistingExtendedAuthentication(TENANT_ID, "", "", "", ELECTION_EVENT);
    }

    @Test
    public void testNonExistingVotingCardId2() throws Exception {
        expectedException.expect(ResourceNotFoundException.class);
        expectedException.expectMessage("Not found Extended Authentication with Id");

        when(authenticationRepository.getForUpdate(any(), any(), any())).thenReturn(Optional.empty());

        sut.authenticate(TENANT_ID, VOTING_CARD_ID_2, EXTRA_PARAM, ELECTION_EVENT);
    }

    @Test
    public void authenticateFailsOnUpdate()
            throws ApplicationException, ResourceNotFoundException, AuthenticationException,
            AllowedAttemptsExceededException, EntryPersistenceException, GeneralCryptoLibException {

        expectedException.expect(AuthenticationException.class);

        ExtendedAuthentication extendedAuthentication =
            createEntity(TENANT_ID, PDKDF_2, ELECTION_EVENT, ENCRYPTED_START_VOTING_KEY, SALT, 0);
        when(authenticationRepository.getForUpdate(any(), any(), any()))
            .thenReturn(Optional.of(extendedAuthentication));
        when(trackIdInstance.getTrackId()).thenReturn("");
        when(authenticationRepository.update(any(ExtendedAuthentication.class)))
            .thenThrow(new EntryPersistenceException("exception"));
        sut.authenticate(TENANT_ID, VOTING_CARD_ID_2, INVALID, ELECTION_EVENT);
    }

    private ExtendedAuthentication createEntity(String tenantId, String encryptedExtraParam, String electionEvent,
            String encryptedStartVotingKey, String salt, Integer attempts) {
        ExtendedAuthentication extendedAuthentication = new ExtendedAuthentication();
        extendedAuthentication.setAttempts(attempts);
        extendedAuthentication.setElectionEvent(electionEvent);
        extendedAuthentication.setEncryptedStartVotingKey(encryptedStartVotingKey);
        extendedAuthentication.setExtraParam(encryptedExtraParam);
        extendedAuthentication.setTenantId(tenantId);
        extendedAuthentication.setSalt(salt);
        return extendedAuthentication;
    }

    private CryptoAPIX509Certificate createCertificate() throws IOException, GeneralCryptoLibException {
        final KeyPair keyPairForSigning = CryptoUtils.getKeyPairForSigning();
        return CryptoUtils.createCryptoAPIx509Certificate(COMMON_NAME, CertificateParameters.Type.SIGN,
            keyPairForSigning);
    }

    private Answer<Object> defaultAnswer() {
        return new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                Object result;
                TransactionalAction<?> action = invocation.getArgumentAt(0, TransactionalAction.class);
                TransactionContext context = mock(TransactionContext.class);
                try {
                    result = action.execute(context);
                } catch (RuntimeException e) {
                    throw e;
                } catch (Exception e) {
                    throw new TransactionalActionException(e);
                }
                return result;
            }
        };
    }

    @Test
    public void testSaveOrUpdateExtendedAuthenticationFromFileSuccess()
            throws IOException, TransactionalActionException, EntryPersistenceException, ApplicationException,
            GeneralCryptoLibException, RetrofitException {

        final Path tempFile = Files.createTempFile("testSecureLogWhenEntryPersistenceException.csv", null);

        FileUtils.writeStringToFile(tempFile.toFile(),
            "authId1,extraParam1,encryptedStartVotingKey1,electionEvent1,salt1,credentialId1" + System.lineSeparator()
                + "authId2,extraParam2,encryptedStartVotingKey2,electionEvent2,salt2,credentialId2"
                + System.lineSeparator()
                + "authId1,extraParam1,encryptedStartVotingKey1,electionEvent1,salt1,credentialId1");

        ExtendedAuthentication extendedAuthentication = new ExtendedAuthentication();
        extendedAuthentication.setAuthId("authId1");
        extendedAuthentication.setExtraParam("extraParam1");
        extendedAuthentication.setEncryptedStartVotingKey("encryptedStartVotingKey1");
        extendedAuthentication.setElectionEvent("electionEvent1");
        extendedAuthentication.setSalt("salt1");
        extendedAuthentication.setCredentialId("credentialId1");
        mockCertificate();
        when(verifier.verify(any(PublicKey.class), any(Path.class))).thenReturn(true);

        when(transactionController.doInNewTransaction(any(TransactionalAction.class))).then(defaultAnswer());

        when(authenticationRepository.getForUpdate(anyString(), anyString(), anyString()))
            .thenReturn(Optional.ofNullable(null));

        when(authenticationRepository.getForUpdate(TENANT_ID, "authId1", "electionEvent1"))
            .thenReturn(Optional.of(extendedAuthentication));

        sut.saveExtendedAuthenticationFromFile(tempFile, TENANT_ID, ELECTION_EVENT, "adminBoardId");

    }

    @Test
    public void testExportVotingCardsReturnsDataIfCredentialsAreBlocked() throws Exception {
        String tenantId = "tenantId";
        String electionEventId = "electionEventId";
        String credentialId = "credentialId";
        String votingCardId = "votingCardId";

        VoterInformation testVoterInformation = new VoterInformation();
        testVoterInformation.setElectionEventId(electionEventId);
        testVoterInformation.setCredentialId(credentialId);
        testVoterInformation.setVotingCardId(votingCardId);
        when(
            voterMaterialService.getVoterInformationByCredentialId(eq(tenantId), eq(electionEventId), eq(credentialId)))
                .thenReturn(Optional.of(testVoterInformation));

        final ExtendedAuthentication mockExtendedAuthenticationEntity = mock(ExtendedAuthentication.class);
        when(mockExtendedAuthenticationEntity.getTenantId()).thenReturn(tenantId);
        when(mockExtendedAuthenticationEntity.getElectionEvent()).thenReturn(electionEventId);
        when(mockExtendedAuthenticationEntity.getCredentialId()).thenReturn(credentialId);

        when(
            authenticationRepository.findAllExceededExtendedAuthentication(eq(tenantId), eq(electionEventId), anyInt()))
                .thenReturn(Collections.singletonList(mockExtendedAuthenticationEntity));

        byte[] bytes;
        try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
            sut.findAndWriteVotingCardsWithFailedAuthentication(tenantId, electionEventId, outStream);
            bytes = outStream.toByteArray();
        }

        String stringData = new String(bytes, StandardCharsets.UTF_8);
        Assert.assertTrue(stringData.contains(votingCardId));
    }

    @Test
    public void testExportVotingCardsReturnsEmptyIfNoCredentialsAreBlocked() throws Exception {

        String tenantId = "tenantId";
        String electionEventId = "electionEventId";

        when(authenticationRepository.findAllExceededExtendedAuthentication(anyString(), any(), anyInt()))
            .thenReturn(Collections.emptyList());

        byte[] bytes;
        try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) {
            sut.findAndWriteVotingCardsWithFailedAuthentication(tenantId, electionEventId, outStream);
            bytes = outStream.toByteArray();
        }

        Assert.assertEquals(0, bytes.length);
        String stringData = new String(bytes, StandardCharsets.UTF_8);
        Assert.assertTrue(stringData.isEmpty());
    }

    private void mockCertificate() throws IOException, GeneralCryptoLibException, RetrofitException {
        final CryptoAPIX509Certificate certificate = createCertificate();
        when(certificateEntity.getCertificateContent())
            .thenReturn(new String(certificate.getPemEncoded(), StandardCharsets.UTF_8));
        when(remoteCertificateService.getAdminBoardCertificate(anyString())).thenReturn(certificateEntity);
    }

    @Test
    public void testAuthenticateInvalidExtraParamsNoExceptionThrown()
            throws ApplicationException, ResourceNotFoundException, AuthenticationException,
            AllowedAttemptsExceededException, GeneralCryptoLibException {
        String authId = "authId1";
        ExtendedAuthentication extendedAuthentication = new ExtendedAuthentication();
        extendedAuthentication.setAuthId(authId);
        extendedAuthentication.setSalt("");
        extendedAuthentication.setExtraParam(null);

        when(authenticationRepository.getForUpdate(TENANT_ID, authId, ELECTION_EVENT))
            .thenReturn(Optional.of(extendedAuthentication));

        sut.authenticate(TENANT_ID, authId, null, ELECTION_EVENT);

    }

}
