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

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.products.ov.commons.beans.Ballot;
import com.scytl.products.ov.commons.verify.MetadataFileVerifier;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.tallying.command.TallyingHolder;
import com.scytl.products.ov.tallying.path.PathResolver;
import com.scytl.products.ov.tallying.webapp.mvc.BallotInformation;
import com.scytl.products.ov.tallying.webapp.mvc.ContestInformation;
import com.scytl.products.ov.tallying.webapp.mvc.GeneratingUtils;
import com.scytl.products.ov.tallying.webapp.mvc.TallyingEngineController;
import com.scytl.products.ov.tallying.webapp.mvc.dto.input.TallyingWebappInput;
import com.scytl.products.ov.tallying.webapp.spring.TargetPathResolverConfig;
import com.scytl.products.ov.tallying.webapp.spring.WebAppTestConfig;

import cucumber.api.java.en.And;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;

@ContextConfiguration(classes = {WebAppTestConfig.class, TargetPathResolverConfig.class })
@WebAppConfiguration
public class SpringIntegrationTest {

    private static final String CSV_ROW_DELIMITER_IN_STORY_FILE = "\t";

    private static final String EMPTY = "EMPTY";

    private MockMvc mockMvc;

    @Autowired
    private TallyingEngineController controller;

    @Autowired
    private PathResolver pathResolver;

    private Map<String, Map<Integer, String>> ballotBoxContents = new HashMap<>();

    private Map<String, Map<Integer, String>> auditableVotes = new HashMap<>();

    private Map<String, Map<Integer, String>> failedVotes = new HashMap<>();

    private Map<Integer, BallotInformation> ballotBoxConfigurations = new HashMap<>();

    private KeyPair keyPair;

    @Given("^a decrypted ballot box with contestId (.+) with id (\\d+) and with decryptedBallots: (.+)")
    public void createDecryptedBallotContentForBallotBox(String contestId, int ballotId, String ballotContent)
            throws GeneralCryptoLibException, IOException, URISyntaxException {
        ballotBoxContents.clear();
        ballotBoxConfigurations.clear();

        Path certificatePath = pathResolver.resolve("").resolve("csr").resolve("certId.pem");
        File file = certificatePath.toFile();
        file.getParentFile().mkdirs();
        keyPair = GeneratingUtils.generateAndStoreCertificateAndReturnKeyPair("certId", certificatePath);
        andCreateDecryptedBallotContentForBallotBox(contestId, ballotId, ballotContent);
    }

    @Given("^a decrypted ballot box with contestId (.+) with id (\\d+) and with auditableVotes: (.+)")
    public void createAuditableVotesContentForBallotBox(String contestId, int ballotId, String auditableVotesContent)
            throws GeneralCryptoLibException, IOException, URISyntaxException {
        auditableVotes.clear();
        andCreateAuditableVotesContentForBallotBox(contestId, ballotId, auditableVotesContent);
    }

    private void andCreateAuditableVotesContentForBallotBox(String contestId, int ballotId,
            String auditableVotesContent) {
        if (auditableVotes.containsKey(contestId)) {
            auditableVotes.get(contestId).put(ballotId, auditableVotesContent);
        } else {
            Map<Integer, String> ballotBox = new HashMap<>();
            ballotBox.put(ballotId, auditableVotesContent);
            auditableVotes.put(contestId, ballotBox);
        }

    }

    @Given("^a decrypted ballot box with contestId (.+) with id (\\d+) and with failedVotes: (.+)")
    public void createFailedVotesContentForBallotBox(String contestId, int ballotId, String failedVotesContent)
            throws GeneralCryptoLibException, IOException, URISyntaxException {
        failedVotes.clear();
        andCreateFailedVotesContentForBallotBox(contestId, ballotId, failedVotesContent);
    }

    private void andCreateFailedVotesContentForBallotBox(String contestId, int ballotId, String failedVotesContent) {
        if (failedVotes.containsKey(contestId)) {
            failedVotes.get(contestId).put(ballotId, failedVotesContent);
        } else {
            Map<Integer, String> ballotBox = new HashMap<>();
            ballotBox.put(ballotId, failedVotesContent);
            failedVotes.put(contestId, ballotBox);
        }

    }

    @And("^also a decrypted ballot box with contestId (.+) with id (\\d+) and with decryptedBallots: (.+)")
    public void andCreateDecryptedBallotContentForBallotBox(String contestId, int ballotId, String ballotContent)
            throws GeneralCryptoLibException, IOException, URISyntaxException {
        if (ballotBoxContents.containsKey(contestId)) {
            ballotBoxContents.get(contestId).put(ballotId, ballotContent);
        } else {
            Map<Integer, String> ballotBox = new HashMap<>();
            ballotBox.put(ballotId, ballotContent);
            ballotBoxContents.put(contestId, ballotBox);
        }
    }

    @When("^tallying$")
    public void executeTallyBinding() throws Throwable {
        executeTally();
    }

    @Then("^the outcome upon tallying the ballot box with contestId (.+) with id (\\d+) is: (.+)$")
    public void verifyOutcome(String contestId, int ballotId, String ballotContent)
            throws URISyntaxException, IOException, GeneralCryptoLibException {
        String actualContestId = getActualContestId(contestId);

        Set<Entry<Integer, String>> entrySet = ballotBoxContents.get(actualContestId).entrySet();
        for (Entry<Integer, String> entry : entrySet) {
            if (entry.getKey().equals(ballotId)) {
                BallotInformation ballotInformation = ballotBoxConfigurations.get(entry.getKey());
                checkGeneratedFiles(keyPair.getPublic(), ballotInformation, ballotContent, contestId);
            }
        }
    }

    @Then("^the invalid votes upon tallying the ballot box with contestId (.+) with id (\\d+) is: (.+)$")
    public void verifyInvalidVotes(String contestId, int ballotId, String invalidVotes)
            throws URISyntaxException, IOException, GeneralCryptoLibException {
        String actualContestId = getActualContestId(contestId);

        Set<Entry<Integer, String>> entrySet = ballotBoxContents.get(actualContestId).entrySet();
        for (Entry<Integer, String> entry : entrySet) {
            if (entry.getKey().equals(ballotId)) {
                BallotInformation ballotInformation = ballotBoxConfigurations.get(entry.getKey());
                checkGeneratedInvalidFiles(keyPair.getPublic(), ballotInformation, invalidVotes);
            }
        }
    }

    private String getActualContestId(String contestId) {
        Set<String> keySet = ballotBoxContents.keySet();
        String actualContestId = contestId;
        for (String string : keySet) {
            if (string.contains(contestId)) {
                actualContestId = string;
            }
        }
        return actualContestId;
    }

    private void executeTally() throws Exception {
        this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
        String electionId = UUID.randomUUID().toString();
        Set<Entry<String, Map<Integer, String>>> set = ballotBoxContents.entrySet();
        for (Entry<String, Map<Integer, String>> entry : set) {
            String contestId = entry.getKey();
            Set<Entry<Integer, String>> entrySet = entry.getValue().entrySet();
            for (Entry<Integer, String> ballotContent : entrySet) {

                List<ContestInformation> contests = new ArrayList<ContestInformation>();
                String[] split = contestId.split(",");
                for (String string : split) {
                    contests.add(new ContestInformation(string.trim()));
                }
                BallotInformation bi = new BallotInformation(electionId, ballotContent.getKey().toString(), contests);
                ballotBoxConfigurations.put(ballotContent.getKey(), bi);

                TallyingWebappInput input = new TallyingWebappInput();
                input.setBallot(generateBallot(bi));
                input.setBallotBoxID(bi.getBbId());
                input.setWriteInSeparator("#");
                input.setTenantID("100");
                input.setCertId("certId");
                input.setPrivateKeyPEM(PemUtils.privateKeyToPem(keyPair.getPrivate()));
                Path workingDirectory = getWorkingDirectory(bi.getElectionId(), bi.getBbId(), input.getBallotBoxID());

                TallyingHolder holder = new TallyingHolder();
                holder.setSignatureKey(keyPair.getPrivate());
                holder.setTenantId(input.getTenantID());
                holder.setElectionEventId(bi.getElectionId());
                holder.setBallotBoxId(input.getBallotBoxID());
                holder.setWriteInSeparator("#");
                String ballotContents = ballotContent.getValue();
                ballotContents = EMPTY.equals(ballotContents) ? "" : ballotContents;
                createFileWithContent(workingDirectory.resolve("decryptedBallots.csv"), ballotContents, holder);
                createFileWithContent(workingDirectory.resolve("auditableVotes.csv"),
                    createAuditableVotesContent(contestId, ballotContent), holder);
                createFileWithContent(workingDirectory.resolve("failedVotes.csv"),
                    createFailedVotesContent(contestId, ballotContent), holder);

                RequestBuilder request = post("/tally").content(GeneratingUtils.asJsonString(input))
                    .contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);

                MvcResult result = this.mockMvc.perform(request).andExpect(status().isOk()).andReturn();

                JsonNode errors =
                    new ObjectMapper().readTree(result.getResponse().getContentAsByteArray()).get("errors");

                assertEquals(0, errors.size());

            }
        }
    }

    private String createAuditableVotesContent(String contestId, Entry<Integer, String> ballotContent) {
        String auditableVotesContent = "";
        Map<Integer, String> map = auditableVotes.get(contestId);
        if (map != null) {
            String string = map.get(ballotContent.getKey());
            if (string != null) {
                auditableVotesContent = string;
            }
        }
        return auditableVotesContent;
    }

    private String createFailedVotesContent(String contestId, Entry<Integer, String> ballotContent) {
        String failedVotesContent = "";
        Map<Integer, String> map = failedVotes.get(contestId);
        if (map != null) {
            String string = map.get(ballotContent.getKey());
            if (string != null) {
                failedVotesContent = string;
            }
        }
        return failedVotesContent;
    }

    private void createFileWithContent(Path resolve, String value, TallyingHolder holder)
            throws IOException, GeneralCryptoLibException {
        String realOutput = value.replace(CSV_ROW_DELIMITER_IN_STORY_FILE, System.lineSeparator());
        resolve.toFile().getParentFile().mkdirs();
        try (FileOutputStream fos = new FileOutputStream(resolve.toFile())) {
            fos.write(realOutput.getBytes(StandardCharsets.UTF_8));
        }
        sign(resolve, holder);
    }

    private void sign(Path file, TallyingHolder holder) throws IOException, GeneralCryptoLibException {
        GeneratingUtils.sign(file, holder);
    }

    private Ballot generateBallot(BallotInformation bi) {
        return GeneratingUtils.generateBallot(bi);
    }

    private void checkGeneratedFiles(PublicKey publicKey, BallotInformation bi, String ballotContent, String contestId)
            throws URISyntaxException, IOException, GeneralCryptoLibException {
        Path workingDirectory = getWorkingDirectory(bi.getElectionId(), bi.getBbId(), bi.getBbId());
        File talliedVotesFile =
            workingDirectory.resolve("tally").resolve(contestId).resolve(contestId + ".csv").toFile();
        List<String> talliedVotes = Files.readAllLines(talliedVotesFile.toPath(), StandardCharsets.UTF_8);
        String[] split =
            EMPTY.equals(ballotContent) ? new String[0] : ballotContent.split(CSV_ROW_DELIMITER_IN_STORY_FILE);
        assertEquals(split.length, talliedVotes.size());
        for (int i = 0; i < split.length; i++) {
            assertEquals(split[i], talliedVotes.get(i));
        }
        checkSignature(talliedVotesFile, publicKey);

    }

    private Path getWorkingDirectory(String electionId, String bid, String bbId) {
        Path workingDirectory = pathResolver.resolve("").resolve(electionId).resolve(Constants.ONLINE_DIRECTORY)
            .resolve(Constants.ELECTION_INFORMATION_DIRECTORY).resolve(Constants.BALLOTS_DIRECTORY).resolve(bid)
            .resolve(Constants.BALLOT_BOXES_DIRECTORY).resolve(bbId);
        return workingDirectory;
    }

    private void checkGeneratedInvalidFiles(PublicKey publicKey, BallotInformation bi, String ballotContent)
            throws URISyntaxException, IOException, GeneralCryptoLibException {
        Path workingDirectory = getWorkingDirectory(bi.getElectionId(), bi.getBbId(), bi.getBbId());
        File talliedVotesFile = workingDirectory.resolve("tally").resolve("invalidVotes.csv").toFile();
        List<String> talliedVotes = Files.readAllLines(talliedVotesFile.toPath(), StandardCharsets.UTF_8);
        String[] split =
            EMPTY.equals(ballotContent) ? new String[0] : ballotContent.split(CSV_ROW_DELIMITER_IN_STORY_FILE);
        assertEquals(split.length, talliedVotes.size());
        for (int i = 0; i < split.length; i++) {
            assertEquals(split[i], talliedVotes.get(i));
        }
        checkSignature(talliedVotesFile, publicKey);

    }

    private void checkSignature(File file, PublicKey publicKey) throws IOException, GeneralCryptoLibException {
        final Path metadataFilePath = file.toPath().resolveSibling(file.getName() + ".metadata");

        try (FileInputStream csvFileIn = new FileInputStream(file);
                FileInputStream metadataFileIn = new FileInputStream(metadataFilePath.toFile())) {

            boolean success =
                new MetadataFileVerifier(new AsymmetricService()).verifySignature(publicKey, metadataFileIn, csvFileIn);
            assertTrue(success);
        }
    }

}
