/*
 * 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.vw.services.domain.service;

import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.CONFIRMATION_ATTEMPTS;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.INFO_BALLOT_BOX_ID;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.INFO_ERR_DESC;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.INFO_HASH_CONFIRMATION_MESSAGE;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.INFO_STATUS_VOTING_CARD;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogConstants.INFO_TRACK_ID;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogEvents.CONFIRMATION_MESSAGE_VALIDATION_INCREASE_ATTEMPTS;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogEvents.CONFIRMATION_MESSAGE_VALIDATION_INVALID_VOTING_CARD_STATE;
import static com.scytl.products.ov.vw.infrastructure.log.VotingWorkflowLogEvents.CONFIRMATION_MESSAGE_VALIDATION_MAX_ATTEMPTS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.util.Map;

import javax.ejb.EJBException;
import javax.json.JsonObject;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.slf4j.Logger;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.products.oscore.logging.api.domain.Level;
import com.scytl.products.oscore.logging.api.domain.LogContent;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
import com.scytl.products.ov.commons.beans.domain.model.confirmation.ConfirmationMessage;
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.validation.CryptographicOperationException;
import com.scytl.products.ov.commons.beans.validation.ValidationErrorType;
import com.scytl.products.ov.commons.dto.CastCodeAndComputeResults;
import com.scytl.products.ov.commons.logging.service.AuthTokenHashService;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
import com.scytl.products.ov.commons.ui.ws.rs.ObjectMappers;
import com.scytl.products.ov.commons.validation.ValidationError;
import com.scytl.products.ov.vw.infrastructure.log.ConfirmationMessageHashService;
import com.scytl.products.ov.vw.services.domain.model.authentication.AuthenticationToken;
import com.scytl.products.ov.vw.services.domain.model.confirmation.ConfirmationInformation;
import com.scytl.products.ov.vw.services.domain.model.confirmation.ConfirmationInformationRepository;
import com.scytl.products.ov.vw.services.domain.model.confirmation.ConfirmationInformationResult;
import com.scytl.products.ov.vw.services.domain.model.confirmation.VoteCastCodeRepository;
import com.scytl.products.ov.vw.services.domain.model.confirmation.VoteCastMessage;
import com.scytl.products.ov.vw.services.domain.model.confirmation.VoteCastResult;
import com.scytl.products.ov.vw.services.domain.model.information.VoterInformation;
import com.scytl.products.ov.vw.services.domain.model.state.VotingCardState;
import com.scytl.products.ov.vw.services.domain.model.state.VotingCardStates;

/**
 * Tests of {@link CastVoteServiceImpl}.
 */
@RunWith(MockitoJUnitRunner.class)
public class CastVoteServiceImplTest {
    private static final String AUTHENTICATION_TOKEN = "{\"signature\":\"value\"}";

    private VoterInformation voter;

    private ConfirmationInformation confirmation;

    private VotingCardState state;

    private ConfirmationInformationResult confirmationResult;

    private CastCodeAndComputeResults codeComputationResult;

    private VoteCastResult result;

    @Mock
    private VotingCardStateService stateService;

    @Mock
    private VoteCastCodeRepository codeRepository;

    @Mock
    private VoteCastCodeService codeService;

    @Mock
    private ConfirmationInformationRepository confirmationRepository;

    @Mock
    private AuthTokenHashService tokenHashService;

    @Mock
    private ConfirmationMessageHashService confirmationHashService;

    @Mock
    private SecureLoggingWriter secureLogger;

    @Mock
    private Logger logger;

    @Mock
    private TrackIdInstance trackIdInstance;

    @InjectMocks
    private CastVoteServiceImpl service = new CastVoteServiceImpl();

    @Before
    public void setUp()
            throws GeneralCryptoLibException, ApplicationException, CryptographicOperationException,
            ResourceNotFoundException, IOException, DuplicateEntryException {
        voter = new VoterInformation();
        voter.setBallotBoxId("ballotBoxId");
        voter.setBallotId("ballotId");
        voter.setCredentialId("credentialId");
        voter.setElectionEventId("electionEventId");
        voter.setId(Integer.valueOf(1));
        voter.setTenantId("tenantId");
        voter.setVerificationCardId("verificationCardId");
        voter.setVerificationCardSetId("verificationCardSetId");
        voter.setVotingCardId("votingCardId");
        voter.setVotingCardSetId("votingCardSetId");

        confirmation = new ConfirmationInformation();
        confirmation.setCertificate("certificate");
        confirmation.setCredentialId("credentialId");
        ConfirmationMessage confirmationMessage = new ConfirmationMessage();
        confirmationMessage.setConfirmationKey("confirmationKey");
        confirmationMessage.setSignature("signature");
        confirmation.setConfirmationMessage(confirmationMessage);

        state = new VotingCardState();
        state.setAttempts(0L);
        state.setElectionEventId(voter.getElectionEventId());
        state.setTenantId(voter.getTenantId());
        state.setVotingCardId(voter.getVotingCardId());
        state.setState(VotingCardStates.SENT_BUT_NOT_CAST);

        confirmationResult = new ConfirmationInformationResult();
        confirmationResult.setElectionEventId(voter.getElectionEventId());
        confirmationResult.setValid(true);
        confirmationResult.setVotingCardId(voter.getVotingCardId());

        codeComputationResult = new CastCodeAndComputeResults();
        codeComputationResult.setSignature("signature");
        codeComputationResult.setVoteCastCode("voteCastCode");

        result = new VoteCastResult();
        result.setElectionEventId(state.getElectionEventId());
        result.setValid(true);
        result.setVerificationCardId(voter.getVerificationCardId());
        result.setVoteCastMessage(
            new VoteCastMessage(codeComputationResult.getVoteCastCode(), codeComputationResult.getSignature()));
        result.setVotingCardId(state.getVotingCardId());

        when(stateService.getVotingCardState(voter.getTenantId(), voter.getElectionEventId(), voter.getVotingCardId()))
            .thenReturn(state);
        // this is actual behavior of the dependency
        doAnswer(i -> {
            state.incrementAttempts();
            return null;
        }).when(stateService).incrementVotingCardAttempts(voter.getTenantId(), voter.getElectionEventId(),
            voter.getVotingCardId());
        AuthenticationToken authenticationTokenObject =
            ObjectMappers.fromJson(AUTHENTICATION_TOKEN, AuthenticationToken.class);
        when(codeRepository.generateCastCode(state.getTenantId(), state.getElectionEventId(),
            voter.getVerificationCardId(), voter.getVotingCardId(), authenticationTokenObject.getSignature(),
            confirmationMessage)).thenReturn(codeComputationResult);
        when(codeRepository.storesCastCode(state.getTenantId(), state.getElectionEventId(), state.getVotingCardId(),
            codeComputationResult)).thenReturn(true);

        when(codeService.generateVoteCastResult(state.getTenantId(), state.getElectionEventId(),
            state.getVotingCardId(), voter.getVerificationCardId(), codeComputationResult)).thenReturn(result);

        when(confirmationRepository.validateConfirmationMessage(state.getTenantId(), state.getElectionEventId(),
            state.getVotingCardId(), confirmation, AUTHENTICATION_TOKEN)).thenReturn(confirmationResult);

        when(tokenHashService.hash(any(JsonObject.class))).thenReturn("tokenHash");

        when(confirmationHashService.hash(any(ConfirmationInformation.class))).thenReturn("confirmationHash");

        when(trackIdInstance.getTrackId()).thenReturn("trackId");
    }

    @Test
    public void testCastVote() throws ResourceNotFoundException, ApplicationException, DuplicateEntryException {
        assertEquals(result, service.castVote(AUTHENTICATION_TOKEN, voter, confirmation));
        verify(stateService).incrementVotingCardAttempts(state.getTenantId(), state.getElectionEventId(),
            state.getVotingCardId());
        verify(stateService).updateVotingCardState(state.getTenantId(), state.getElectionEventId(),
            state.getVotingCardId(), VotingCardStates.CAST);
        verify(codeRepository).storesCastCode(state.getTenantId(), state.getElectionEventId(), state.getVotingCardId(),
            codeComputationResult);
    }

    @Test
    public void testCastVoteFailedLastAttempt()
            throws ResourceNotFoundException, ApplicationException, DuplicateEntryException,
            CryptographicOperationException {
        state.setAttempts(4L);
        when(codeRepository.generateCastCode(anyString(), anyString(), anyString(), anyString(), anyString(),
            any(ConfirmationMessage.class))).thenThrow(new CryptographicOperationException("test"));
        VoteCastResult result = service.castVote(AUTHENTICATION_TOKEN, voter, confirmation);
        assertFalse(result.isValid());
        ValidationError error = result.getValidationError();
        assertEquals(ValidationErrorType.BCK_ATTEMPTS_EXCEEDED, error.getValidationErrorType());
        assertEquals(voter.getElectionEventId(), result.getElectionEventId());
        assertEquals(voter.getVotingCardId(), result.getVotingCardId());
        assertEquals(voter.getVerificationCardId(), result.getVerificationCardId());
        assertNull(result.getVoteCastMessage());
        verify(stateService).incrementVotingCardAttempts(state.getTenantId(), state.getElectionEventId(),
            state.getVotingCardId());
        verify(stateService).updateVotingCardState(state.getTenantId(), state.getElectionEventId(),
            state.getVotingCardId(), VotingCardStates.WRONG_BALLOT_CASTING_KEY);
        verify(secureLogger).log(eq(Level.ERROR), argThat(new ArgumentMatcher<LogContent>() {
            @Override
            public boolean matches(final Object argument) {
                LogContent content = (LogContent) argument;
                assertEquals(CONFIRMATION_MESSAGE_VALIDATION_MAX_ATTEMPTS, content.getLogEvent());
                assertEquals("tokenHash", content.getObjectId());
                assertEquals(voter.getVotingCardId(), content.getUser());
                assertEquals(voter.getElectionEventId(), content.getElectionEvent());
                Map<String, String> info = content.getAdditionalInfo();
                assertEquals("trackId", info.get(INFO_TRACK_ID));
                assertEquals("confirmationHash", info.get(INFO_HASH_CONFIRMATION_MESSAGE));
                assertEquals(voter.getBallotBoxId(), info.get(INFO_BALLOT_BOX_ID));
                assertTrue(info.containsKey(INFO_ERR_DESC));
                return true;
            }
        }));
    }

    @Test
    public void testCastVoteWrongBallotCastingKeyState()
            throws ResourceNotFoundException, ApplicationException, DuplicateEntryException {
        state.setState(VotingCardStates.WRONG_BALLOT_CASTING_KEY);
        VoteCastResult result = service.castVote(AUTHENTICATION_TOKEN, voter, confirmation);
        assertFalse(result.isValid());
        ValidationError error = result.getValidationError();
        assertEquals(ValidationErrorType.BCK_ATTEMPTS_EXCEEDED, error.getValidationErrorType());
        assertEquals(voter.getElectionEventId(), result.getElectionEventId());
        assertEquals(voter.getVotingCardId(), result.getVotingCardId());
        assertEquals(voter.getVerificationCardId(), result.getVerificationCardId());
        assertNull(result.getVoteCastMessage());
        verify(secureLogger).log(eq(Level.ERROR), argThat(new ArgumentMatcher<LogContent>() {
            @Override
            public boolean matches(final Object argument) {
                LogContent content = (LogContent) argument;
                assertEquals(CONFIRMATION_MESSAGE_VALIDATION_MAX_ATTEMPTS, content.getLogEvent());
                assertEquals("tokenHash", content.getObjectId());
                assertEquals(voter.getVotingCardId(), content.getUser());
                assertEquals(voter.getElectionEventId(), content.getElectionEvent());
                Map<String, String> info = content.getAdditionalInfo();
                assertEquals("trackId", info.get(INFO_TRACK_ID));
                assertEquals("confirmationHash", info.get(INFO_HASH_CONFIRMATION_MESSAGE));
                assertEquals(voter.getBallotBoxId(), info.get(INFO_BALLOT_BOX_ID));
                assertTrue(info.containsKey(INFO_ERR_DESC));
                return true;
            }
        }));
    }

    @Test
    public void testCastVoteIllegalState()
            throws ResourceNotFoundException, ApplicationException, DuplicateEntryException {
        state.setState(VotingCardStates.CAST);
        VoteCastResult result = service.castVote(AUTHENTICATION_TOKEN, voter, confirmation);
        assertFalse(result.isValid());
        assertEquals(voter.getElectionEventId(), result.getElectionEventId());
        assertEquals(voter.getVotingCardId(), result.getVotingCardId());
        assertEquals(voter.getVerificationCardId(), result.getVerificationCardId());
        assertNull(result.getVoteCastMessage());
        verify(secureLogger).log(eq(Level.ERROR), argThat(new ArgumentMatcher<LogContent>() {
            @Override
            public boolean matches(final Object argument) {
                LogContent content = (LogContent) argument;
                assertEquals(CONFIRMATION_MESSAGE_VALIDATION_INVALID_VOTING_CARD_STATE, content.getLogEvent());
                assertEquals("tokenHash", content.getObjectId());
                assertEquals(voter.getVotingCardId(), content.getUser());
                assertEquals(voter.getElectionEventId(), content.getElectionEvent());
                Map<String, String> info = content.getAdditionalInfo();
                assertEquals("trackId", info.get(INFO_TRACK_ID));
                assertEquals(state.getState().name(), info.get(INFO_STATUS_VOTING_CARD));
                assertTrue(info.containsKey(INFO_ERR_DESC));
                return true;
            }
        }));
    }

    @Test
    public void testCastVoteFailedConfirmation()
            throws ResourceNotFoundException, ApplicationException, DuplicateEntryException {
        state.setAttempts(2L);
        confirmationResult.setValid(false);
        VoteCastResult result = service.castVote(AUTHENTICATION_TOKEN, voter, confirmation);
        assertFalse(result.isValid());
        ValidationError error = result.getValidationError();
        assertEquals(ValidationErrorType.WRONG_BALLOT_CASTING_KEY, error.getValidationErrorType());
        assertEquals(voter.getElectionEventId(), result.getElectionEventId());
        assertEquals(voter.getVotingCardId(), result.getVotingCardId());
        assertEquals(voter.getVerificationCardId(), result.getVerificationCardId());
        assertNull(result.getVoteCastMessage());
        verify(stateService, never()).incrementVotingCardAttempts(state.getTenantId(), state.getElectionEventId(),
            state.getVotingCardId());
        verify(secureLogger).log(eq(Level.WARN), argThat(new ArgumentMatcher<LogContent>() {
            @Override
            public boolean matches(final Object argument) {
                LogContent content = (LogContent) argument;
                assertEquals(CONFIRMATION_MESSAGE_VALIDATION_INCREASE_ATTEMPTS, content.getLogEvent());
                assertEquals("tokenHash", content.getObjectId());
                assertEquals(voter.getVotingCardId(), content.getUser());
                assertEquals(voter.getElectionEventId(), content.getElectionEvent());
                Map<String, String> info = content.getAdditionalInfo();
                assertEquals("trackId", info.get(INFO_TRACK_ID));
                assertEquals("confirmationHash", info.get(INFO_HASH_CONFIRMATION_MESSAGE));
                assertEquals("3", info.get(CONFIRMATION_ATTEMPTS));
                assertTrue(info.containsKey(INFO_ERR_DESC));
                return true;
            }
        }));
    }

    @Test
    public void testCastVoteFailedToGenerateVoteCastMessage()
            throws CryptographicOperationException, ResourceNotFoundException, ApplicationException,
            DuplicateEntryException, JsonParseException, JsonMappingException, IOException {
        state.setAttempts(2L);
        AuthenticationToken authenticationTokenObject =
            ObjectMappers.fromJson(AUTHENTICATION_TOKEN, AuthenticationToken.class);
        when(codeRepository.generateCastCode(state.getTenantId(), state.getElectionEventId(),
            voter.getVerificationCardId(), voter.getVotingCardId(), authenticationTokenObject.getSignature(),
            confirmation.getConfirmationMessage())).thenThrow(new CryptographicOperationException("test"));
        VoteCastResult result = service.castVote(AUTHENTICATION_TOKEN, voter, confirmation);
        assertFalse(result.isValid());
        ValidationError error = result.getValidationError();
        assertEquals(ValidationErrorType.WRONG_BALLOT_CASTING_KEY, error.getValidationErrorType());
        assertEquals(voter.getElectionEventId(), result.getElectionEventId());
        assertEquals(voter.getVotingCardId(), result.getVotingCardId());
        assertEquals(voter.getVerificationCardId(), result.getVerificationCardId());
        assertNull(result.getVoteCastMessage());
        verify(stateService).incrementVotingCardAttempts(state.getTenantId(), state.getElectionEventId(),
            state.getVotingCardId());
        verify(secureLogger).log(eq(Level.WARN), argThat(new ArgumentMatcher<LogContent>() {
            @Override
            public boolean matches(final Object argument) {
                LogContent content = (LogContent) argument;
                assertEquals(CONFIRMATION_MESSAGE_VALIDATION_INCREASE_ATTEMPTS, content.getLogEvent());
                assertEquals("tokenHash", content.getObjectId());
                assertEquals(voter.getVotingCardId(), content.getUser());
                assertEquals(voter.getElectionEventId(), content.getElectionEvent());
                Map<String, String> info = content.getAdditionalInfo();
                assertEquals("trackId", info.get(INFO_TRACK_ID));
                assertEquals("confirmationHash", info.get(INFO_HASH_CONFIRMATION_MESSAGE));
                assertEquals("2", info.get(CONFIRMATION_ATTEMPTS));
                assertTrue(info.containsKey(INFO_ERR_DESC));
                return true;
            }
        }));
    }

    @Test(expected = EJBException.class)
    public void testCastVoteFailedToStoreVoteCastMessage() throws ResourceNotFoundException {
        when(codeRepository.storesCastCode(state.getTenantId(), state.getElectionEventId(), voter.getVotingCardId(),
            codeComputationResult)).thenReturn(false);
        service.castVote(AUTHENTICATION_TOKEN, voter, confirmation);
    }

    @Test(expected = EJBException.class)
    public void testCastVoteFailedToGenerateVoteCastResult() throws ResourceNotFoundException, IOException {
        when(codeService.generateVoteCastResult(state.getTenantId(), state.getElectionEventId(),
            state.getVotingCardId(), voter.getVerificationCardId(), codeComputationResult))
                .thenThrow(new IOException("test"));
        service.castVote(AUTHENTICATION_TOKEN, voter, confirmation);
    }
}
