/*
 * 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.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

import org.apache.commons.io.FileUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatcher;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

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.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.infrastructure.transaction.TransactionalActionException;
import com.scytl.products.ov.commons.tracking.TrackIdInstance;
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.services.infrastructure.logging.ExtendedAuthenticationLogConstants;
import com.scytl.products.ov.ea.services.infrastructure.logging.ExtendedAuthenticationLogEvents;

@RunWith(MockitoJUnitRunner.class)
public class ExtendedAuthenticationServiceDecoratorTest extends ExtendedAuthenticationTest {

    private static final String TENANT_ID = "tenantId";

    private static final String ELECTION_EVENT_ID = "electionEventId";

    private static final String AUTH_ID = "authId";

    private static final String EXTRA_PARAM = "extraParam";

    @InjectMocks
    private ExtendedAuthenticationServiceDecorator extendedAuthenticationServiceDecorator;

    @Spy
    private SecureLoggingWriter secureLoggerWriter = getSecureLoggingWriter();

    @Mock
    private TrackIdInstance trackIdInstance;

    @Mock
    private ExtendedAuthenticationService extendedAuthenticationService;

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

    @BeforeClass
    public static void setup() {
        ExtendedAuthenticationTest.setup();
    }

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

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

        // noinspection unchecked
        when(extendedAuthenticationService.authenticate(TENANT_ID, AUTH_ID, "extraParam", ELECTION_EVENT_ID))
            .thenThrow(new ApplicationException("exception"));

        try {
            extendedAuthenticationServiceDecorator.authenticate(TENANT_ID, AUTH_ID, "extraParam", ELECTION_EVENT_ID);
            Assert.fail();

        } catch (ApplicationException e) {
            assertSecureLogContainsMessage("ERROR",
                ExtendedAuthenticationLogEvents.EAAUTH_ERROR_INVALID_ATTEMPT.getInfo(),
                ExtendedAuthenticationLogConstants.INFO_TRACK_ID, ExtendedAuthenticationLogConstants.ERR_DESC,
                "Attempt is not valid", "trackId");
        }
    }

    @Test
    public void testSaveExtendedAuthenticationFromFileSuccess()
            throws IOException, TransactionalActionException, EntryPersistenceException, ApplicationException {

        final Path tempFilePath = Files.createTempFile("testSecureLogWhenEntryPersistenceException.csv", null);
        FileUtils.writeStringToFile(tempFilePath.toFile(), "1,2,4,5,6" + System.lineSeparator() + "1,2,3,4,5");

        extendedAuthenticationServiceDecorator.saveExtendedAuthenticationFromFile(tempFilePath, TENANT_ID,
            ELECTION_EVENT_ID, "adminBoardId");

        assertSecureLogContainsMessage("INFO", ExtendedAuthenticationLogEvents.EAMULSTO_SUCCESS.getInfo(),
            ExtendedAuthenticationLogConstants.INFO_TRACK_ID, ExtendedAuthenticationLogConstants.INFO_ADMIN_BOARD_ID);

    }

    @Test(expected = IOException.class)
    public void testSaveExtendedAuthenticationFromFileFailsIO()
            throws IOException, TransactionalActionException, EntryPersistenceException, ApplicationException {

        final Path tempFilePath = Files.createTempFile("testSecureLogWhenEntryPersistenceException.csv", null);
        FileUtils.writeStringToFile(tempFilePath.toFile(), "1,2,4,5,6" + System.lineSeparator() + "1,2,3,4,5");

        doThrow(IOException.class).when(extendedAuthenticationService).saveExtendedAuthenticationFromFile(tempFilePath,
            TENANT_ID, ELECTION_EVENT_ID, "adminBoardId");

        extendedAuthenticationServiceDecorator.saveExtendedAuthenticationFromFile(tempFilePath, TENANT_ID,
            ELECTION_EVENT_ID, "adminBoardId");
    }

    @Test
    public void testRenewExistingExtendedAuthenticationSuccess()
            throws ResourceNotFoundException, ApplicationException, IOException {

        ExtendedAuthentication extendedAuthentication = new ExtendedAuthentication();
        extendedAuthentication.setCredentialId("credentialId");
        extendedAuthentication.setElectionEvent(ELECTION_EVENT_ID);

        when(extendedAuthenticationService.renewExistingExtendedAuthentication(TENANT_ID, "oldAuthId", "newAuthId",
            "newSVK", ELECTION_EVENT_ID)).thenReturn(extendedAuthentication);

        extendedAuthenticationServiceDecorator.renewExistingExtendedAuthentication(TENANT_ID, "oldAuthId", "newAuthId",
            "newSVK", ELECTION_EVENT_ID);

        assertSecureLogContainsMessage("INFO", ExtendedAuthenticationLogEvents.EARENEW_SAVE_SUCCESS.getInfo(),
            ExtendedAuthenticationLogConstants.INFO_TRACK_ID, ExtendedAuthenticationLogConstants.INFO_CREDENTIAL_ID);

    }

    @Test
    public void testRenewExistingExtendedAuthenticationFail()
            throws ResourceNotFoundException, ApplicationException, IOException {

        // noinspection unchecked
        when(extendedAuthenticationService.renewExistingExtendedAuthentication(TENANT_ID, "oldAuthId", "newAuthId",
            "newSVK", ELECTION_EVENT_ID)).thenThrow(new ApplicationException("exception"));

        try {
            extendedAuthenticationServiceDecorator.renewExistingExtendedAuthentication(TENANT_ID, "oldAuthId",
                "newAuthId", "newSVK", ELECTION_EVENT_ID);
            Assert.fail();

        } catch (ApplicationException e) {
            assertSecureLogContainsMessage("ERROR", ExtendedAuthenticationLogEvents.EARENEW_ERROR_SAVING_DATA.getInfo(),
                ExtendedAuthenticationLogConstants.INFO_TRACK_ID, ExtendedAuthenticationLogConstants.ERR_DESC);

        }
    }

    @Test
    public void testUpdateExistingExtendedAuthenticationSuccess() throws EntryPersistenceException, IOException {
        ExtendedAuthentication extendedAuthentication = new ExtendedAuthentication();
        extendedAuthentication.setCredentialId("credentialId");
        extendedAuthentication.setElectionEvent(ELECTION_EVENT_ID);

        extendedAuthenticationServiceDecorator.updateExistingExtendedAuthentication(extendedAuthentication);

        assertSecureLogContainsMessage("INFO", ExtendedAuthenticationLogEvents.EAUPDATE_SUCCESS.getInfo(),
            ExtendedAuthenticationLogConstants.INFO_TRACK_ID, ExtendedAuthenticationLogConstants.INFO_CREDENTIAL_ID);

    }

    @Test
    public void testUpdateExistingExtendedAuthenticationFail() throws EntryPersistenceException, IOException {
        ExtendedAuthentication extendedAuthentication = new ExtendedAuthentication();
        extendedAuthentication.setCredentialId("credentialId");
        extendedAuthentication.setElectionEvent(ELECTION_EVENT_ID);

        doThrow(EntryPersistenceException.class).when(extendedAuthenticationService)
            .updateExistingExtendedAuthentication(extendedAuthentication);

        try {
            extendedAuthenticationServiceDecorator.updateExistingExtendedAuthentication(extendedAuthentication);

        } catch (EntryPersistenceException e) {
            assertSecureLogContainsMessage("ERROR", ExtendedAuthenticationLogEvents.EAUPDATE_ERROR_UPDATING.getInfo(),
                ExtendedAuthenticationLogConstants.INFO_TRACK_ID, ExtendedAuthenticationLogConstants.INFO_CREDENTIAL_ID,
                ExtendedAuthenticationLogConstants.ERR_DESC);
        }
    }

    @Test
    public void testSaveNewExtendedAuthenticationSuccess() throws DuplicateEntryException, IOException {

        ExtendedAuthentication extendedAuthentication = new ExtendedAuthentication();
        extendedAuthentication.setCredentialId("credentialId");
        extendedAuthentication.setElectionEvent(ELECTION_EVENT_ID);

        extendedAuthenticationServiceDecorator.saveNewExtendedAuthentication(extendedAuthentication);

        assertSecureLogContainsMessage("INFO", ExtendedAuthenticationLogEvents.EASAVE_SUCCESS.getInfo(),
            ExtendedAuthenticationLogConstants.INFO_TRACK_ID, ExtendedAuthenticationLogConstants.INFO_CREDENTIAL_ID);
    }

    @Test
    public void testSaveNewExtendedAuthenticationFail() throws DuplicateEntryException, IOException {

        ExtendedAuthentication extendedAuthentication = new ExtendedAuthentication();
        extendedAuthentication.setCredentialId("credentialId");
        extendedAuthentication.setElectionEvent(ELECTION_EVENT_ID);

        doThrow(DuplicateEntryException.class).when(extendedAuthenticationService)
            .saveNewExtendedAuthentication(extendedAuthentication);

        try {
            extendedAuthenticationServiceDecorator.saveNewExtendedAuthentication(extendedAuthentication);
            Assert.fail();

        } catch (DuplicateEntryException e) {
            assertSecureLogContainsMessage("ERROR", ExtendedAuthenticationLogEvents.EASAVE_ERROR_SAVING.getInfo(),
                ExtendedAuthenticationLogConstants.INFO_TRACK_ID, ExtendedAuthenticationLogConstants.INFO_CREDENTIAL_ID,
                ExtendedAuthenticationLogConstants.ERR_DESC);
        }
    }

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

        LogContent expectedLogContent =
            (new LogContent.LogContentBuilder()).logEvent(ExtendedAuthenticationLogEvents.EAAUTH_SUCCESS)
                .objectId(AUTH_ID).user(AUTH_ID).electionEvent(ELECTION_EVENT_ID).createLogInfo();

        EncryptedSVK encryptedStartVotingKey = new EncryptedSVK("encryptedSVK");

        when(extendedAuthenticationService.authenticate(any(), any(), any(), any()))
            .thenReturn(encryptedStartVotingKey);

        extendedAuthenticationServiceDecorator.authenticate(TENANT_ID, AUTH_ID, EXTRA_PARAM, ELECTION_EVENT_ID);

        assertSecureLogContainsMessage("INFO", ExtendedAuthenticationLogEvents.EAAUTH_SUCCESS.getInfo(),
            ExtendedAuthenticationLogConstants.INFO_TRACK_ID);
        // then
        Mockito.verify(secureLoggerWriter).log(eq(Level.INFO),
            argThat(new SuccessLogContentMatcher(expectedLogContent)));
    }

    @Test
    public void test_whenAuthAttemptsReachMaximumSecureLoggerRegistersEvent() throws Exception {

        // given
        expectedException.expect(AllowedAttemptsExceededException.class);

        LogContent expected = (new LogContent.LogContentBuilder())
            .logEvent(ExtendedAuthenticationLogEvents.EAAUTH_ERROR_MAX_ATTEMPTS_REACHED).objectId(AUTH_ID).user(AUTH_ID)
            .electionEvent(ELECTION_EVENT_ID).createLogInfo();

        when(extendedAuthenticationService.authenticate(any(), any(), any(), eq(ELECTION_EVENT_ID)))
            .thenThrow(new AllowedAttemptsExceededException());

        // when
        extendedAuthenticationServiceDecorator.authenticate(TENANT_ID, AUTH_ID, EXTRA_PARAM, ELECTION_EVENT_ID);

        // then
        Mockito.verify(secureLoggerWriter).log(eq(Level.ERROR),
            argThat(new AttemptsExceededErrorLogContentMatcher(expected)));
    }

    public class AttemptsExceededErrorLogContentMatcher extends ArgumentMatcher<LogContent> {

        private LogContent expected;

        AttemptsExceededErrorLogContentMatcher(LogContent expected) {
            this.expected = expected;
        }

        @Override
        public boolean matches(final Object o) {
            LogContent actual = (LogContent) o;
            return actual.getLogEvent().getAction().equalsIgnoreCase(expected.getLogEvent().getAction())
                && actual.getLogEvent().getOutcome().equalsIgnoreCase(expected.getLogEvent().getOutcome())
                && actual.getLogEvent().getInfo().equalsIgnoreCase(expected.getLogEvent().getInfo())
                && actual.getUser().equalsIgnoreCase(expected.getUser())
                && actual.getElectionEvent().equalsIgnoreCase(expected.getElectionEvent())
                && actual.getObjectId().equalsIgnoreCase(expected.getObjectId())
                && actual.getAdditionalInfo().get(ExtendedAuthenticationLogConstants.ERR_DESC)
                    .equalsIgnoreCase(expected.getAdditionalInfo().get(ExtendedAuthenticationLogConstants.ERR_DESC));
        }
    }

    public class SuccessLogContentMatcher extends ArgumentMatcher<LogContent> {

        private LogContent expected;

        SuccessLogContentMatcher(LogContent expected) {
            this.expected = expected;
        }

        @Override
        public boolean matches(final Object o) {
            LogContent actual = (LogContent) o;

            return actual.getLogEvent().getAction().equalsIgnoreCase(expected.getLogEvent().getAction())
                && actual.getLogEvent().getOutcome().equalsIgnoreCase(expected.getLogEvent().getOutcome())
                && actual.getLogEvent().getInfo().equalsIgnoreCase(expected.getLogEvent().getInfo())
                && actual.getUser().equalsIgnoreCase(expected.getUser())
                && actual.getElectionEvent().equalsIgnoreCase(expected.getElectionEvent())
                && actual.getObjectId().equalsIgnoreCase(expected.getObjectId());
        }
    }

}
