/*
 * 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.tallying.api;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import com.scytl.cryptolib.api.asymmetric.AsymmetricServiceAPI;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.certificates.bean.CertificateData;
import com.scytl.cryptolib.certificates.bean.ValidityDates;
import com.scytl.cryptolib.certificates.bean.X509DistinguishedName;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.certificates.service.CertificatesService;
import com.scytl.cryptolib.primitives.service.PrimitivesService;
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.LoggingWriter;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
import com.scytl.products.ov.commons.beans.Ballot;
import com.scytl.products.ov.commons.beans.Contest;
import com.scytl.products.ov.commons.sign.MetadataFileSigner;
import com.scytl.products.ov.commons.sign.SignatureMetadata;
import com.scytl.products.ov.commons.verify.MetadataFileVerifier;
import com.scytl.products.ov.tallying.api.dto.output.TallyingOutput;
import com.scytl.products.ov.tallying.command.TallyingHolder;
import com.scytl.products.ov.tallying.commons.HashService;
import com.scytl.products.ov.tallying.commons.MappingStrategy;
import com.scytl.products.ov.tallying.commons.beans.AuditableVote;
import com.scytl.products.ov.tallying.commons.beans.ContestVote;
import com.scytl.products.ov.tallying.commons.beans.FailedVote;
import com.scytl.products.ov.tallying.commons.exception.TallyingException;
import com.scytl.products.ov.tallying.commons.io.reader.BallotsReader;
import com.scytl.products.ov.tallying.commons.io.reader.DecryptedBallotEntryParser;
import com.scytl.products.ov.tallying.commons.io.writer.SignResultWriter;

import mockit.Mock;
import mockit.MockUp;

public class TallyingServiceTest {

    private static AsymmetricServiceAPI asymmetricService;

    private static MetadataFileSigner metadataFileSigner = new MetadataFileSigner(asymmetricService);

    private static TallyingService _tallyingService;

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

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

        asymmetricService = new AsymmetricService();

        _tallyingService = new TallyingServiceImpl(new SignResultWriter(metadataFileSigner),
            new HashService(new PrimitivesService()), new SecureLoggingWriter() {
                @Override
                public void log(LogContent content) {

                }

                @Override
                public void log(Level level, LogContent content, LoggingWriter loggingWriter) {

                }

                @Override
                public void log(Level level, LogContent logContent) {

                }
            });

    }

    @AfterClass
    public static void removeBouncyCastleProvider() {
        Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
    }

    @Test
    public void perform_the_tallying_correctly() throws TallyingException, IOException, GeneralCryptoLibException {

        final File decryptedOutputPath = new File("target/test-classes/decryptedBallots.csv");

        final TallyingHolder tallyingHolder = initTallyingHolder(decryptedOutputPath, folder.getRoot());

        new MockUp<MetadataFileSigner>() {
            @Mock
			public SignatureMetadata createSignature(final PrivateKey signingKey, final InputStream originalStream,
					final LinkedHashMap<String, String> signedFields) {

                return new SignatureMetadata.SignatureMetadataBuilder().withSignature("").build();
            }

        };

        final TallyingOutput output = new TallyingOutput();

        _tallyingService.tallyValidVotes(tallyingHolder, output);

        assertTrue(output.getErrors().isEmpty());

        checkTalliedVotes(output);
    }

    @Test
    public void perform_the_invalid_votes_tallying_correctly()
            throws TallyingException, IOException, GeneralCryptoLibException {

        final File decryptedOutputPath = new File("target/test-classes/decryptedBallots.csv");

        final TallyingHolder tallyingHolder = initTallyingHolder(decryptedOutputPath, folder.getRoot());

        new MockUp<MetadataFileSigner>() {
            @Mock
            public SignatureMetadata createSignature(final PrivateKey signingKey, final InputStream originalStream,
                    final LinkedHashMap<String, String> signedFields) {

                return new SignatureMetadata.SignatureMetadataBuilder().withSignature("").build();

            }

        };

        final TallyingOutput output = new TallyingOutput();

        _tallyingService.tallyInvalidVotes(tallyingHolder, output);

        assertTrue(output.getErrors().isEmpty());

        assertEquals(2, output.getTalliedInvalidVotes().getNumberOfAuditableVotes());
        assertEquals(1, output.getTalliedInvalidVotes().getNumberOfFailedVotes());

    }

    private void checkTalliedVotes(TallyingOutput output) {
        final List<ContestVote> result = output.getContestVotes();

        assertThat(result, not(nullValue()));
        assertThat(result.size(), is(6));

        List<ContestVote> sortedResult =
            result.stream().sorted((res1, res2) -> res1.getMultiplication().compareTo(res2.getMultiplication()))
                .collect(Collectors.toList());

        assertThat(sortedResult.get(0).getMultiplication().intValue(), is(2));
        assertThat(sortedResult.get(0).getRepresentations().size(), is(1));
        assertThat(sortedResult.get(0).getRepresentations().get(0).getRepresentation(), is("2"));
        assertThat(sortedResult.get(0).getCount(), is(3));
        assertThat(sortedResult.get(0).getContestId(), is("d1d24859cbef408b816a1467b0b3afc7"));

        assertThat(sortedResult.get(1).getMultiplication().intValue(), is(3));
        assertThat(sortedResult.get(1).getRepresentations().size(), is(1));
        assertThat(sortedResult.get(1).getRepresentations().get(0).getRepresentation(), is("3"));
        assertThat(sortedResult.get(1).getCount(), is(1));
        assertThat(sortedResult.get(1).getContestId(), is("d1d24859cbef408b816a1467b0b3afc7"));

        assertThat(sortedResult.get(2).getMultiplication().intValue(), is(5));
        assertThat(sortedResult.get(2).getRepresentations().size(), is(1));
        assertThat(sortedResult.get(2).getRepresentations().get(0).getRepresentation(), is("5"));
        assertThat(sortedResult.get(2).getCount(), is(1));
        assertThat(sortedResult.get(2).getContestId(), is("d1d24859cbef408b816a1467b0b3afc7"));

        assertThat(sortedResult.get(3).getMultiplication().intValue(), is(55913));
        assertThat(sortedResult.get(3).getRepresentations().size(), is(4));
        assertThat(sortedResult.get(3).getRepresentations().get(0).getRepresentation(), is("11"));
        assertThat(sortedResult.get(3).getRepresentations().get(1).getRepresentation(), is("13"));
        assertThat(sortedResult.get(3).getRepresentations().get(2).getRepresentation(), is("17"));
        assertThat(sortedResult.get(3).getRepresentations().get(3).getRepresentation(), is("23"));
        assertThat(sortedResult.get(3).getCount(), is(1));
        assertThat(sortedResult.get(3).getContestId(), is("27aa7af87de444758e58623b88431d6b"));

        assertThat(sortedResult.get(4).getMultiplication().intValue(), is(419543));
        assertThat(sortedResult.get(4).getRepresentations().size(), is(4));
        assertThat(sortedResult.get(4).getRepresentations().get(0).getRepresentation(), is("17"));
        assertThat(sortedResult.get(4).getRepresentations().get(1).getRepresentation(), is("23"));
        assertThat(sortedResult.get(4).getRepresentations().get(2).getRepresentation(), is("29"));
        assertThat(sortedResult.get(4).getRepresentations().get(3).getRepresentation(), is("37"));
        assertThat(sortedResult.get(4).getCount(), is(2));
        assertThat(sortedResult.get(4).getContestId(), is("27aa7af87de444758e58623b88431d6b"));

        assertThat(sortedResult.get(5).getMultiplication().intValue(), is(3355271));
        assertThat(sortedResult.get(5).getRepresentations().size(), is(4));
        assertThat(sortedResult.get(5).getRepresentations().get(0).getRepresentation(), is("29"));
        assertThat(sortedResult.get(5).getRepresentations().get(1).getRepresentation(), is("37"));
        assertThat(sortedResult.get(5).getRepresentations().get(2).getRepresentation(), is("53"));
        assertThat(sortedResult.get(5).getRepresentations().get(3).getRepresentation(), is("59"));
        assertThat(sortedResult.get(5).getCount(), is(2));
        assertThat(sortedResult.get(5).getContestId(), is("27aa7af87de444758e58623b88431d6b"));
    }

    @Test
    public void tally_an_invalid_ballot()
            throws TallyingException, URISyntaxException, IOException, GeneralCryptoLibException {

        final URL decryptedUrl = this.getClass().getClassLoader().getResource("incorrectDecryptedBallots.csv");
        final File decryptedOutputPath = new File(decryptedUrl.toURI());
        folder.create();

        final TallyingHolder tallyingHolder = initTallyingHolder(decryptedOutputPath, folder.getRoot());

        new MockUp<MetadataFileVerifier>() {
            @Mock
            public boolean verifySignature(final PublicKey publicKey, final InputStream metadataStream,
                    final InputStream sourceStream) {
                return true;
            }
        };

        new MockUp<MetadataFileSigner>() {
            @Mock
            public SignatureMetadata createSignature(final PrivateKey signingKey, final InputStream originalStream,
                    final LinkedHashMap<String, String> signedFields) {

                return new SignatureMetadata.SignatureMetadataBuilder().withSignature("").build();

            }
        };

        final TallyingOutput output = new TallyingOutput();

        _tallyingService.tallyValidVotes(tallyingHolder, output);

        assertThat(output.getErrors().get(0).getCode(), is("003"));
        assertThat(output.getErrors().get(0).getMessage(), is(
            "The decrypted ballot box has been tampered with and contains the representation \"42\", which is not present in the ballot"));

        assertThat(output.getErrors().get(1).getCode(), is("003"));
        assertThat(output.getErrors().get(1).getMessage(), is(
            "The decrypted ballot box has been tampered with and contains the representation \"1984\", which is not present in the ballot"));
    }

    private Ballot generateBallot() {
        return GeneratingUtils.generateBallot("f58a52111f2c41e18b5e6d25b07076f9", "81d8fb95d446496997939fdcb064f95a",
            "d1d24859cbef408b816a1467b0b3afc7", "alias_1", "votation", "27aa7af87de444758e58623b88431d6b", "alias_2s",
            "election");
    }

    private TallyingHolder initTallyingHolder(final File decryptedOutputPath, final File tallyingOutputDirPath)
            throws TallyingException, IOException, GeneralCryptoLibException {

        final BallotsReader ballotsReader = new BallotsReader(new DecryptedBallotEntryParser());

        final MappingStrategy mappingStrategy = new MappingStrategy();

        final Ballot ballot = generateBallot();

        final List<List<String>> decryptedValues = ballotsReader.read(decryptedOutputPath.toPath(), "id", "bbid");

        final Map<String, Contest> optionContestMappings = mappingStrategy.mapBallot(ballot);

        X509Certificate signatureCert = generateTestCertAndSign(decryptedOutputPath.toPath());

        ArrayList<AuditableVote> auditableVotes = new ArrayList<>();
        auditableVotes.add(new AuditableVote("1", "1", "1"));
        auditableVotes.add(new AuditableVote("2", "2", "2"));

        ArrayList<FailedVote> failedVotes = new ArrayList<>();
        failedVotes.add(new FailedVote("3", "3", "3"));

        TallyingHolder holder = new TallyingHolder();
        holder.setDecryptedBallotsPath(decryptedOutputPath.toPath());
        holder.setDecryptedValues(decryptedValues);
        holder.setAuditableVotesPath(null);
        holder.setAuditableValues(auditableVotes);
        holder.setFailedVotesPath(null);
        holder.setFailedValues(failedVotes);
        holder.setInvalidVotesFileName("invalidVotes.csv");
        holder.setOptionContestMappings(optionContestMappings);
        holder.setOutputDir(Paths.get(tallyingOutputDirPath.getAbsolutePath(), "tally"));
        holder.setSignatureCert(signatureCert);
        holder.setTenantId("100");
        holder.setElectionEventId("1234");
        holder.setBallotBoxId("1234");
        holder.setWriteInSeparator("#");
        holder.setContests(ballot.getContests());

        return holder;
    }

    private X509Certificate generateTestCertAndSign(final Path path) throws GeneralCryptoLibException, IOException {
        KeyPair keyPair = new AsymmetricService().getKeyPairForSigning();

        ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
        ZonedDateTime end = now.plusYears(1);
        ValidityDates validityDates = new ValidityDates(Date.from(now.toInstant()), Date.from(end.toInstant()));

        CertificateData certificateData = new CertificateData();
        certificateData.setSubjectPublicKey(keyPair.getPublic());
        certificateData.setValidityDates(validityDates);
        X509DistinguishedName distinguishedName = new X509DistinguishedName.Builder("certId", "ES").build();
        certificateData.setSubjectDn(distinguishedName);
        certificateData.setIssuerDn(distinguishedName);

        CryptoAPIX509Certificate cert =
            new CertificatesService().createSignX509Certificate(certificateData, keyPair.getPrivate());

        Path signaturePath = Paths.get(path.toString() + ".metadata");
        String signatureB64 = "";
        Files.deleteIfExists(signaturePath);
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(signaturePath.toFile()))) {
            writer.write(signatureB64);
        }

        return cert.getCertificate();
    }
}
