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

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
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.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;

import javax.persistence.TypedQuery;

import org.hibernate.Query;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.junit.Assert;
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.Matchers;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import org.slf4j.Logger;
import com.opencsv.CSVWriter;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.products.ov.commons.beans.domain.model.receipt.Receipt;
import com.scytl.products.ov.commons.beans.domain.model.vote.Vote;
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.ResourceNotFoundException;
import com.scytl.products.ov.commons.beans.exceptions.ValidationException;
import com.scytl.products.ov.commons.dto.ComputeResults;
import com.scytl.products.ov.commons.logging.service.VoteHashService;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.AdditionalData;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBox;
import com.scytl.products.ov.ei.services.domain.model.ballotbox.BallotBoxRepository;
import com.scytl.products.ov.ei.services.domain.model.validation.VoteValidation;
import com.scytl.products.ov.ei.services.domain.model.validation.VoteValidationRepository;
import com.scytl.products.ov.ei.services.domain.model.votingcard.VotingCardWriter;

/**
 * The class <code>VoteRepositoryImplTest</code> contains tests for the class
 * {@link <code>VoteRepositoryImpl</code>}
 */
@RunWith(MockitoJUnitRunner.class)
public class BallotBoxRepositoryImplTest {

    public static final String VOTING_CARD_ID = "1";
    @Mock
    protected EntityManager entityManagerMock;

    @Mock
    private TypedQuery<BallotBox> queryBBMock;

    @Mock
    private TypedQuery<String> queryStringMock;

    @Mock
    private VoteValidationRepository voteValidationRepository;

    @Spy
    private BallotBoxFactory ballotBoxFactory = new BallotBoxFactory();

    @Mock
    private VoteHashService voteHashService;

    @Mock
    private Logger LOG;

    @Spy
    @InjectMocks
    private static BallotBoxRepository ballotBoxRepository = new BallotBoxRepositoryImpl();

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

    private static final String encryptedOptions =
        "132412342134;asdfasdfasdf.AVeryLongStringWithTheEncryptedOptionsafasdf12312423asdfnmlourjvlmaldjfoqwer14ad34fas1avjsjjj;.";

    @Test
    public void saveBBNull() throws ApplicationException, DuplicateEntryException, IOException, ValidationException {
        Vote vote = null;
        Receipt receipt = null;
        String authenticationToken = null;
        ComputeResults computeResults = null;

        expectedException.expect(NullPointerException.class);

        ballotBoxRepository.save(ballotBoxFactory.from(vote, computeResults, receipt, authenticationToken, null));
    }

    @Test
    public void saveBBFailedPersistenceException()
            throws ApplicationException, DuplicateEntryException, IOException, ValidationException,
            GeneralCryptoLibException, ResourceNotFoundException {
        String ballotId = "";
        String ballotBoxId = "";
        String tenantId = "";

        Vote vote = new Vote();
        vote.setTenantId(tenantId);
        vote.setBallotId(ballotId);
        vote.setBallotBoxId(ballotBoxId);
        vote.setEncryptedOptions(encryptedOptions);
        
        ComputeResults computationResults = new ComputeResults();
        computationResults.setComputationResults("computationResults");
        computationResults.setDecryptionResults("decryptionResults");

        Receipt receipt = new Receipt();
        String authenticationToken =
            "{\"id\":\"fF8vg9sZna6OXhVtDHL1Vw==\",\"voterInformation\":{\"tenantId\":\"100\",\"electionEventId\":\"100\",\"votingCardId\":\"b421dc7d1bb50fd06c3327d126113818\",\"ballotId\":\"100\",\"credentialId\":\"100\",\"verificationCardId\":\"100\",\"ballotBoxId\":\"100\",\"votingCardSetId\":\"100\",\"verificationCardSetId\":\"100\"},\"timestamp\":\"${__time()}\",\"signature\":\"US669kapfNwGrc3LeoZoTbTT80zarHNnsR0KelfPiC0MlvFZjMVWnEcdH+DEcf6HSXYVbn6MhwmGacOYwu2c/zDPTc1GUE3lDiqwkfc0EQp1+SMwNrKxEJWqk8sUe1cApnUmwWzBQsypxycqsayu8yVg/RtVqzHv/JrIoDG7d3Cdw43NisvJBgHmDNNpmnj7ZJJzJRz+eh8REC8x3bHSxLARwjqFtW1+2ugQJfs1eGwN4blBkHTK5rbulZ+h8R5dw/nXXxsq80gZSX359mo3kxaa6OF6Pt4sVwju5BHceQse6gIkSs8Ol1d9nbyRxOsyXbEYvo0NW9ei7TrIzmwYbA==\"}";

        when(voteHashService.hash(any(Vote.class))).thenReturn("hash");
        when(voteValidationRepository.findByTenantIdElectionEventIdVotingCardId(anyString(), anyString(), anyString(),
            anyString())).thenReturn(new VoteValidation());

        String exceptionMessage = "Persistence Exception Message";
        doThrow(new PersistenceException(exceptionMessage)).when(ballotBoxRepository)
            .save(Matchers.any(BallotBox.class));

        expectedException.expect(PersistenceException.class);
        expectedException.expectMessage(exceptionMessage);

        ballotBoxRepository.save(ballotBoxFactory.from(vote, computationResults, receipt, authenticationToken, null));
    }

    @Test
    public void saveBB() throws ApplicationException, DuplicateEntryException, IOException, ValidationException {
        String ballotId = "ballotId";
        String ballotBoxId = "ballotBoxId";
        String tenantId = "tenantId";

        Vote vote = new Vote();
        vote.setTenantId(tenantId);
        vote.setBallotId(ballotId);
        vote.setBallotBoxId(ballotBoxId);
        vote.setEncryptedOptions(encryptedOptions);
        
        ComputeResults computationResults = new ComputeResults();
        computationResults.setComputationResults("computationResults");
        computationResults.setDecryptionResults("decryptionResults");

        
        Receipt receipt = new Receipt();
        String authenticationToken =
            "{\"id\":\"fF8vg9sZna6OXhVtDHL1Vw==\",\"voterInformation\":{\"tenantId\":\"100\",\"electionEventId\":\"100\",\"votingCardId\":\"b421dc7d1bb50fd06c3327d126113818\",\"ballotId\":\"100\",\"credentialId\":\"100\",\"verificationCardId\":\"100\",\"ballotBoxId\":\"100\",\"votingCardSetId\":\"100\",\"verificationCardSetId\":\"100\"},\"timestamp\":\"${__time()}\",\"signature\":\"US669kapfNwGrc3LeoZoTbTT80zarHNnsR0KelfPiC0MlvFZjMVWnEcdH+DEcf6HSXYVbn6MhwmGacOYwu2c/zDPTc1GUE3lDiqwkfc0EQp1+SMwNrKxEJWqk8sUe1cApnUmwWzBQsypxycqsayu8yVg/RtVqzHv/JrIoDG7d3Cdw43NisvJBgHmDNNpmnj7ZJJzJRz+eh8REC8x3bHSxLARwjqFtW1+2ugQJfs1eGwN4blBkHTK5rbulZ+h8R5dw/nXXxsq80gZSX359mo3kxaa6OF6Pt4sVwju5BHceQse6gIkSs8Ol1d9nbyRxOsyXbEYvo0NW9ei7TrIzmwYbA==\"}";

        doReturn(new BallotBox()).when(ballotBoxRepository).save(Matchers.any(BallotBox.class));

        assertNotNull(ballotBoxRepository.save(ballotBoxFactory.from(vote, computationResults, receipt, authenticationToken, null)));
    }

    @Test
    public void saveBBAdditionalData()
            throws ApplicationException, DuplicateEntryException, IOException, ValidationException {
        String ballotId = "ballotId";
        String ballotBoxId = "ballotBoxId";
        String tenantId = "tenantId";

        Vote vote = new Vote();
        vote.setTenantId(tenantId);
        vote.setBallotId(ballotId);
        vote.setBallotBoxId(ballotBoxId);
        vote.setEncryptedOptions(encryptedOptions);
        
        ComputeResults computationResults = new ComputeResults();
        computationResults.setComputationResults("computationResults");
        computationResults.setDecryptionResults("decryptionResults");
        
        Receipt receipt = new Receipt();
        String authenticationToken =
            "{\"id\":\"fF8vg9sZna6OXhVtDHL1Vw==\",\"voterInformation\":{\"tenantId\":\"100\",\"electionEventId\":\"100\",\"votingCardId\":\"b421dc7d1bb50fd06c3327d126113818\",\"ballotId\":\"100\",\"credentialId\":\"100\",\"verificationCardId\":\"100\",\"ballotBoxId\":\"100\",\"votingCardSetId\":\"100\",\"verificationCardSetId\":\"100\"},\"timestamp\":\"${__time()}\",\"signature\":\"US669kapfNwGrc3LeoZoTbTT80zarHNnsR0KelfPiC0MlvFZjMVWnEcdH+DEcf6HSXYVbn6MhwmGacOYwu2c/zDPTc1GUE3lDiqwkfc0EQp1+SMwNrKxEJWqk8sUe1cApnUmwWzBQsypxycqsayu8yVg/RtVqzHv/JrIoDG7d3Cdw43NisvJBgHmDNNpmnj7ZJJzJRz+eh8REC8x3bHSxLARwjqFtW1+2ugQJfs1eGwN4blBkHTK5rbulZ+h8R5dw/nXXxsq80gZSX359mo3kxaa6OF6Pt4sVwju5BHceQse6gIkSs8Ol1d9nbyRxOsyXbEYvo0NW9ei7TrIzmwYbA==\"}";

        List<AdditionalData> additionalData = new ArrayList<AdditionalData>();
        String key = "key1";
        String value = "value1";
        additionalData.add(new AdditionalData(key, value));

        doReturn(new BallotBox()).when(ballotBoxRepository).save(Matchers.any(BallotBox.class));

        assertNotNull(
            ballotBoxRepository.save(ballotBoxFactory.from(vote, computationResults, receipt, authenticationToken, additionalData)));
    }

    @Test
    public void saveBBEmptyAdditionalData()
            throws ApplicationException, DuplicateEntryException, IOException, ValidationException {
        String ballotId = "ballotId";
        String ballotBoxId = "ballotBoxId";
        String tenantId = "tenantId";

        Vote vote = new Vote();
        vote.setTenantId(tenantId);
        vote.setBallotId(ballotId);
        vote.setBallotBoxId(ballotBoxId);
        vote.setEncryptedOptions(encryptedOptions);
        
        ComputeResults computationResults = new ComputeResults();
        computationResults.setComputationResults("computationResults");
        computationResults.setDecryptionResults("decryptionResults");
        
        Receipt receipt = new Receipt();
        String authenticationToken =
            "{\"id\":\"fF8vg9sZna6OXhVtDHL1Vw==\",\"voterInformation\":{\"tenantId\":\"100\",\"electionEventId\":\"100\",\"votingCardId\":\"b421dc7d1bb50fd06c3327d126113818\",\"ballotId\":\"100\",\"credentialId\":\"100\",\"verificationCardId\":\"100\",\"ballotBoxId\":\"100\",\"votingCardSetId\":\"100\",\"verificationCardSetId\":\"100\"},\"timestamp\":\"${__time()}\",\"signature\":\"US669kapfNwGrc3LeoZoTbTT80zarHNnsR0KelfPiC0MlvFZjMVWnEcdH+DEcf6HSXYVbn6MhwmGacOYwu2c/zDPTc1GUE3lDiqwkfc0EQp1+SMwNrKxEJWqk8sUe1cApnUmwWzBQsypxycqsayu8yVg/RtVqzHv/JrIoDG7d3Cdw43NisvJBgHmDNNpmnj7ZJJzJRz+eh8REC8x3bHSxLARwjqFtW1+2ugQJfs1eGwN4blBkHTK5rbulZ+h8R5dw/nXXxsq80gZSX359mo3kxaa6OF6Pt4sVwju5BHceQse6gIkSs8Ol1d9nbyRxOsyXbEYvo0NW9ei7TrIzmwYbA==\"}";

        List<AdditionalData> additionalData = new ArrayList<AdditionalData>();

        doReturn(new BallotBox()).when(ballotBoxRepository).save(Matchers.any(BallotBox.class));

        assertNotNull(
            ballotBoxRepository.save(ballotBoxFactory.from(vote, computationResults, receipt, authenticationToken, additionalData)));
    }

    @Test
    public void findByTenantIdElectionEventIdVotingCardId() throws ResourceNotFoundException {
        when(entityManagerMock.createQuery(Matchers.anyString(), eq(BallotBox.class))).thenReturn(queryBBMock);
        when(queryBBMock.setParameter(Matchers.anyString(), Matchers.anyString())).thenReturn(queryBBMock);
        ArrayList<BallotBox> result = new ArrayList<BallotBox>();
        result.add(new BallotBox());
        when(queryBBMock.getResultList()).thenReturn(result);

        String tenantId = "2";
        String votingCardId = "2";
        String electionEventId = "2";
        assertNotNull(
            ballotBoxRepository.findByTenantIdElectionEventIdVotingCardId(tenantId, electionEventId, votingCardId));
    }

    @Test
    public void findByTenantIdElectionEventIdVotingCardIdNotFound() throws ResourceNotFoundException {
        when(entityManagerMock.createQuery(Matchers.anyString(), eq(BallotBox.class))).thenReturn(queryBBMock);
        when(queryBBMock.setParameter(Matchers.anyString(), Matchers.anyString())).thenReturn(queryBBMock);
        ArrayList<BallotBox> result = new ArrayList<BallotBox>();
        when(queryBBMock.getResultList()).thenReturn(result);

        expectedException.expect(ResourceNotFoundException.class);

        String tenantId = "2";
        String votingCardId = "2";
        String electionEventId = "2";
        ballotBoxRepository.findByTenantIdElectionEventIdVotingCardId(tenantId, electionEventId, votingCardId);
    }

    @Test
    public void findByTenantIdElectionEventIdNotEmpty() {
        when(entityManagerMock.createQuery(Matchers.anyString(), eq(BallotBox.class))).thenReturn(queryBBMock);
        when(queryBBMock.setParameter(Matchers.anyString(), Matchers.anyString())).thenReturn(queryBBMock);
        ArrayList<BallotBox> result = new ArrayList<BallotBox>();
        result.add(new BallotBox());
        when(queryBBMock.getResultList()).thenReturn(result);

        String tenantId = "2";
        String electionEventId = "2";
        String ballotBoxId = "3";
        assertNotNull(
            ballotBoxRepository.findByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId, ballotBoxId));
        assertTrue(!ballotBoxRepository.findByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId, ballotBoxId)
            .isEmpty());
    }

    @Test
    public void findByTenantIdElectionEventIdEmpty() {
        when(entityManagerMock.createQuery(Matchers.anyString(), eq(BallotBox.class))).thenReturn(queryBBMock);
        when(queryBBMock.setParameter(Matchers.anyString(), Matchers.anyString())).thenReturn(queryBBMock);
        when(queryBBMock.getResultList()).thenReturn(new ArrayList<BallotBox>());

        String tenantId = "2";
        String electionEventId = "2";
        String ballotBoxId = "3";
        assertNotNull(
            ballotBoxRepository.findByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId, ballotBoxId));
        assertTrue(ballotBoxRepository.findByTenantIdElectionEventIdBallotBoxId(tenantId, electionEventId, ballotBoxId)
            .isEmpty());
    }

    @Test
    public void writeUsedVotingCardsReturnsEmptyIfNoVotesFound() throws Exception{

        //given
        Session mockSession = mock(Session.class);
        when(entityManagerMock.unwrap(eq(Session.class))).thenReturn(mockSession);
        Query mockQuery = mock(Query.class);
        when(mockQuery.setParameter(anyInt(), any())).thenReturn(mockQuery);
        ScrollableResults mockResults = mock(ScrollableResults.class);
        when(mockResults.next()).thenReturn(false);
        when(mockQuery.scroll(any(ScrollMode.class))).thenReturn(mockResults);
        when(mockSession.createQuery(anyString())).thenReturn(mockQuery);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        VotingCardWriter writer = spy(new VotingCardWriter(baos));
        String tenantId = "100";
        String electionEventId = "100";
        //when
        ballotBoxRepository.findAndWriteUsedVotingCards(tenantId, electionEventId,writer);
        writer.close();

        //then (check that we didn't write anything)
        verify(writer, times(0)).write(any());
        final byte[] bytes = baos.toByteArray();
        //i'm assuming that we write text data (csv)
        Assert.assertEquals("", new String(bytes, StandardCharsets.UTF_8));

    }

    @Test
    public void writeUsedVotingCardsReturnsSomethingIfVotesFound() throws Exception {

        //given
        Session mockSession = mock(Session.class);
        when(entityManagerMock.unwrap(eq(Session.class))).thenReturn(mockSession);
        Query mockQuery = mock(Query.class);
        when(mockQuery.setParameter(anyInt(), any())).thenReturn(mockQuery);
        ScrollableResults mockResults = mock(ScrollableResults.class);
        when(mockResults.next()).thenReturn(true).thenReturn(false);
        when(mockResults.get(eq(0))).thenReturn(VOTING_CARD_ID);
        when(mockQuery.scroll(any(ScrollMode.class))).thenReturn(mockResults);
        when(mockSession.createQuery(anyString())).thenReturn(mockQuery);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        VotingCardWriter writer = spy(new VotingCardWriter(baos));
        String tenantId = "100";
        String electionEventId = "100";

        //when
        ballotBoxRepository.findAndWriteUsedVotingCards(tenantId, electionEventId, writer);
        writer.close();
        final byte[] bytes = baos.toByteArray();
        //i'm assuming that we write text data (csv)
        String actualVotingCardId = new String(bytes, StandardCharsets.UTF_8);

        //then (csv writer always writes value + newline)
        verify(writer, times(1)).write(any());
        Assert.assertEquals(VOTING_CARD_ID+CSVWriter.DEFAULT_LINE_END, actualVotingCardId);

    }
}
