/*
 * 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.validation.services;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.hamcrest.CoreMatchers.containsString;
import static org.mockito.Mockito.mock;

import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.primitives.PrimitivesServiceAPI;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.cryptolib.primitives.service.PrimitivesService;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
import com.scytl.products.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.commons.beans.Ballot;
import com.scytl.products.ov.commons.sign.CryptoTestData;
import com.scytl.products.ov.commons.sign.MetadataFileSigner;
import com.scytl.products.ov.commons.util.ConfigObjectMapper;
import com.scytl.products.ov.encryption.params.generation.ElGamalEncryptionParameters;
import com.scytl.products.ov.validation.ValidationException;
import com.scytl.products.ov.validation.ValidatorConfig;
import com.scytl.products.ov.validation.decompress.GroupElementDecompressor;
import com.scytl.products.ov.validation.decompress.VoteValidator;
import com.scytl.products.ov.validation.services.input.VoteWithProofEntryParser;
import com.scytl.products.ov.validation.services.input.VotesWithProofReader;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

/**
 * Validation service integration test.
 */
public class ValidationServiceITest {

    private static final String WRITE_IN_ALPHABET =
        "IyAnKCksLS4vMDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXrCoMKixaDFocW9xb7FksWTxbjDgMOBw4LDg8OEw4XDhsOHw4jDicOKw4vDjMONw47Dj8OQw5HDksOTw5TDlcOWw5jDmcOaw5vDnMOdw57Dn8Ogw6HDosOjw6TDpcOmw6fDqMOpw6rDq8Osw63DrsOvw7DDscOyw7PDtMO1w7bDuMO5w7rDu8O8w73DvsO/";

    private static final String BALLOT_BOX_ID = "ballotBoxId";

    private static final String TENANT_ID = "tenantId";

    private static final ElGamalEncryptionParameters ENCRYPTION_PARAMETERS =
        new ElGamalEncryptionParameters(CryptoTestData.P, CryptoTestData.Q, CryptoTestData.G);

    private static Path outputPath;

    private ValidatorConfig validatorConfig;

    @Rule
    public final TemporaryFolder temporaryFolder = new TemporaryFolder();

    @BeforeClass
    public static void setUp() throws IOException, GeneralCryptoLibException {
        Security.addProvider(new BouncyCastleProvider());
    }

    @Before
    public void initializeValidatorConfig() throws GeneralCryptoLibException, IOException {
        // Set the location of the config folder the tests will use.
        outputPath = temporaryFolder.getRoot().toPath();

        validatorConfig = new ValidatorConfig(getBallot(), TENANT_ID, BALLOT_BOX_ID,
            ENCRYPTION_PARAMETERS.getP().toString(), getSigningKeyPEM(), WRITE_IN_ALPHABET, outputPath.toString());
    }

    @Test
    public void testValidation() throws ValidationException, IOException, GeneralCryptoLibException {
        List<Path> mixingPaths = prepareMixingPaths("noWriteIns");

        ValidationService sut = createValidationService();

        sut.validate(validatorConfig, mixingPaths);

        // Count the individual votes from each of the mixing paths.
        long votesWithProofCount = mixingPaths.stream().mapToLong(mixingPath -> {
            try {
                return Files.lines(ValidationServiceImpl.getVotesWithProofFilePath(mixingPath)).count();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }).sum();

        // The number of auditable votes should match the input.
        long auditableVotesCount =
            Files.lines(ValidationServiceImpl.getAuditableVotesFilePath(validatorConfig.getOutputPath())).count();
        assertEquals(votesWithProofCount, auditableVotesCount);

        // The number of decrypted votes should be 0.
        long decompressedVotesCount =
            Files.lines(ValidationServiceImpl.getDecompressedVotesFilePath(validatorConfig.getOutputPath())).count();
        assertEquals(0, decompressedVotesCount);
    }

    @Test
    public void testValidationWithWriteIns() throws ValidationException, IOException, GeneralCryptoLibException {
        List<Path> mixingPaths = prepareMixingPaths("writeIns");

        ValidationService sut = createValidationService();
        sut.validate(validatorConfig, mixingPaths);

        // Ensure the decompressed votes file is readable even with UTF-8
        // characters.
        Path decompressedVotesFilePath =
            ValidationServiceImpl.getDecompressedVotesFilePath(validatorConfig.getOutputPath());
        Files.lines(decompressedVotesFilePath).forEach(line -> {
            assertThat(line, containsString("é"));
            assertThat(line, containsString("ñ"));
        });
    }

    /**
     * Prepare a config folder that can be manipulated, away from the project's
     * source.
     */
    private List<Path> prepareMixingPaths(String testName) throws IOException, GeneralCryptoLibException {
        // Copy the config folder somewhere it can be modified.
        FileUtils.copyDirectory(new File("src/test/resources/mixingPaths/" + testName), outputPath.toFile());
        // Set up the mixing paths from the newly copied folder.
        List<Path> mixingPaths = new ArrayList<>();
        // Add only the directories.
        Files.walk(outputPath, 1).filter(Files::isDirectory).forEach(mixingPaths::add);
        // Remove the top-level folder.
        mixingPaths.remove(0);

        return mixingPaths;
    }

    private ValidationService createValidationService() throws IOException, GeneralCryptoLibException {
        // Create an instance of the service.
        return new ValidationServiceImpl(getVotesWithProofReader(getConfigObjectMapper()),
            getGroupElementDecompressor(), getVoteValidator(), getPrimitivesServiceAPI(), getSecureLoggingWriter(),
            getTransactionInfoProvider(), getMetadataFileSigner());
    }

    private ConfigObjectMapper getConfigObjectMapper() {
        return new ConfigObjectMapper();
    }

    private TransactionInfoProvider getTransactionInfoProvider() {
        return new TransactionInfoProvider();
    }

    private SecureLoggingWriter getSecureLoggingWriter() {
        return mock(SecureLoggingWriter.class);
    }

    private PrimitivesServiceAPI getPrimitivesServiceAPI() throws GeneralCryptoLibException {
        return new PrimitivesService();
    }

    private VoteValidator getVoteValidator() {
        return new VoteValidator();
    }

    private AsymmetricServiceAPI getAsymmetricService() throws GeneralCryptoLibException {
        return new AsymmetricService();
    }

    private MetadataFileSigner getMetadataFileSigner() throws GeneralCryptoLibException {
        return new MetadataFileSigner(getAsymmetricService());
    }

    private GroupElementDecompressor getGroupElementDecompressor() {
        return new GroupElementDecompressor();
    }

    private VotesWithProofReader getVotesWithProofReader(ConfigObjectMapper objectMapper) {
        return new VotesWithProofReader(new VoteWithProofEntryParser(objectMapper), getSecureLoggingWriter());
    }

    private String getSigningKeyPEM() throws GeneralCryptoLibException {
        return PemUtils.privateKeyToPem(getAsymmetricService().getKeyPairForSigning().getPrivate());
    }

    private static Ballot getBallot() throws IOException {
        return new ConfigObjectMapper().fromJSONFileToJava(new File("src/test/resources/mixingPaths/ballot.json"),
            Ballot.class);
    }

}
