/*
 * 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.sdm.application.service;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.security.auth.x500.X500Principal;

import mockit.Mock;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.service.AsymmetricService;
import com.scytl.cryptolib.asymmetric.utils.KeyPairConverter;
import com.scytl.products.ov.commons.beans.exceptions.ApplicationException;
import com.scytl.products.ov.commons.beans.exceptions.ResourceNotFoundException;
import com.scytl.products.ov.commons.path.PathResolver;
import com.scytl.products.ov.commons.path.PrefixPathResolver;
import com.scytl.products.ov.config.shares.exception.SharesException;
import com.scytl.products.ov.config.shares.handler.CreateSharesHandler;
import com.scytl.products.ov.config.shares.handler.StatelessReadSharesHandler;
import com.scytl.products.ov.config.shares.keys.rsa.RSAKeyPairGenerator;
import com.scytl.products.ov.config.shares.service.SmartCardService;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.csr.CSRGenerator;
import com.scytl.products.ov.csr.CertificateRequestSigner;
import com.scytl.products.ov.sdm.application.config.SmartCardConfig;
import com.scytl.products.ov.sdm.domain.model.EntityRepository;
import com.scytl.products.ov.sdm.domain.model.administrationauthority.ActivateOutputData;
import com.scytl.products.ov.sdm.domain.model.administrationauthority.AdministrationAuthorityRepository;
import com.scytl.products.ov.sdm.domain.model.status.SmartCardStatus;
import com.scytl.products.ov.sdm.domain.service.ConfigConstants;
import com.scytl.products.ov.sdm.infrastructure.service.ConfigurationEntityStatusService;
import com.scytl.products.ov.utils.PemUtils;

import mockit.Deencapsulation;
import mockit.Expectations;
import mockit.MockUp;
import mockit.Mocked;
import mockit.integration.junit4.JMockit;

@RunWith(JMockit.class)
public class AdminBoardServiceTest {

    public static PKCS10CertificationRequest csr;

    public static String admBoardId;

    public static X500Principal subject;

    private static AdminBoardService sut;

    private static String testMember0 = "Maria";

    private static String testMember1 = "Carla";

    // this is a administration authority json
    private static String administrationAuthorityJSON = "{\n"
        + "            \"id\": \"04d7e562547646c2aec2ef12628dc126\",\n"
        + "            \"defaultTitle\": \"Adminboard\",\n" + "            \"defaultDescription\": \"AsampleAB\",\n"
        + "            \"alias\": \"AB1\",\n" + "            \"minimumThreshold\": \"1\",\n"
        + "            \"status\": \"LOCKED\",\n" + "            \"administrationBoard\": [\n" + "                \""
        + testMember0 + "\",\n" + "                \"" + testMember1 + "\"\n" + "            ]\n" + "        }";

    private static String publicKeyForEncryptionPEM =
        "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA53It4Iqf"
            + "/5vd3EDjfFa3yIA2pxvBYE6sSPhQUHoxNXZFmhhj9RCbknUekEE/WdgsC/+5zti1r7pubLmuwtIbkHyHPIVg5e/Xu"
            + "/hoRtmbTZHWAELDjvRqsD3prNba+t+W0piYA99Wn4Fb1M/Uo6zyqTBgK3Rejtp2bE1w2s/Lzuk"
            + "/GzAKIXzmZk589YMl0hm8JxN2JprPemK0LVXogijR4AmCgzT9fPpZx1QNIzmFisQzYc3EpnlnQ7mRxMjr60laUM5s"
            + "+G00lCfnc2a8ZKOYuJPJjO/8hnNfaEDSNbOto3K92RtE2O+xjhzo1oELTovnPjJKinbZA2deojtSjiaJxQIDAQAB"
            + "-----END PUBLIC KEY-----";

    private static String privateKeyForEncryptionPEM = "-----BEGIN RSA PRIVATE "
        + "KEY-----MIIEpAIBAAKCAQEA53It4Iqf/5vd3EDjfFa3yIA2pxvBYE6sSPhQUHoxNXZFmhhj9RCbknUekEE/WdgsC"
        + "/+5zti1r7pubLmuwtIbkHyHPIVg5e/Xu/hoRtmbTZHWAELDjvRqsD3prNba+t+W0piYA99Wn4Fb1M"
        + "/Uo6zyqTBgK3Rejtp2bE1w2s/Lzuk"
        + "/GzAKIXzmZk589YMl0hm8JxN2JprPemK0LVXogijR4AmCgzT9fPpZx1QNIzmFisQzYc3EpnlnQ7mRxMjr60laUM5s"
        + "+G00lCfnc2a8ZKOYuJPJjO/8hnNfaEDSNbOto3K92RtE2O"
        + "+xjhzo1oELTovnPjJKinbZA2deojtSjiaJxQIDAQABAoIBAQCcmi1gmWvZUGW1"
        + "+lHyd9qy184jFCysNY9tcFcnnQZe3kAKHCbGUw5w8r5TbVKoQBTNqaLXytpkpQjCmIEfYXs1MI1w7e66pqaakWI9TlA"
        + "/FEZwtrwLpmXqCnpqcJaK2W774DQ7qoq6MpUoUdfXR9aJlCn+PSceEcO/VEbgR2nn"
        + "/bEpRycgX2ueJ4HgLt25Vlkirpf2O5IwUkTdWcSTRhOy40sPubOnLupUWJGyDOaa8Xd"
        + "+bn0KS1jH18PMo3C0qvAohA0BtcwpLX5ac/oFCfmk8/IwPXwYhePwxNLLfBgG0lfLQl629gE"
        + "+Avkq57G8vRBYcOD6kwdonDIRxFs+PmQBAoGBAPnWWWU2V+qeYLMmBkJLV9GwRNSNdUvBn4jedApV2bBNKw5PY31B6kvC"
        + "/EoB10FYIYQdat9dSOWncRbzX1jAXvcUqNn0RbLy/B3D4"
        + "/4vV39UeVWtK8OgLkxbBPSVFGwK3Nivuy0rcG2p0hT4yaxUGj1H2Y1hyrI9fAzPPAkL8ILhAoGBAO0nsb2Vb0VpsddAfyn01mhPgXzyNz81NBsxJb9ubgjQNSdhX/XPn36kTwtwpPPuJMWe9yxzlaHf/HxFP0kKhPBaviK1cKDm/gWWR9UEdRKEB9hkr+BIfhYbi+3KSWzmcE8fV0KHdcvV28JINmW95zhRI1dw4bbBR6xD1bE5RsdlAoGAaD9KmfLtCFcBnn8VSYBKqpJUhiRody3ZtbCs1ssvxGLOvm/d4ZwpeWdpAjB2cyulAI2N1JoGGt2dUKhIdq3+cjbKpfdJRfwhuwHMFnoGlnjXECrsAfrKls276ZpzJQn7UOcywQxJI1ki8eFFtYR6VmuumVHe1DTXmDi4okW7G8ECgYEAiQ1xiHB9t42XeyAI3URjTDD2UjDggKTMkhJbEEBPUsSQk0uQ20u7jsKB88iLa3Tqx1JQ4d2CUeRR07dpFVsA7K5kR0a36iTUFIJ+zLogtiybJBE8Gs+KHliZCzjmKgsaSH+CPC5wgvX6ZFK7LR0MLN2nIPdZWfZk50bkjeDd6IkCgYA7yqQhMsen+PWjf2MPh4tTml3zE1MHEPw0JeLnWaheviGFXtaX6U1eLjS/2EIPQShR+YgMeuk6+C31YiUuCKfvVzMtc1L0DQCyqIPE3B1QLnijH8nZvDnJXBZWyVZE57WRnSaa7JshRMC8llkL0Z+jZlDc7C+qgjYfgiDUIpd/eQ==-----END RSA PRIVATE KEY-----";

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

    @Mocked
    PKCS10CertificationRequest certReq;

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

        sut = new AdminBoardService();

        Deencapsulation.setField(sut, "pathResolver", new PrefixPathResolver("target/"));

        Deencapsulation.setField(sut, "asymmetricServiceAPI", new AsymmetricService());

        Deencapsulation.setField(sut, "rsaKeyPairGenerator", new RSAKeyPairGenerator(new AsymmetricService()));

        Deencapsulation.setField(sut, "smartCardConfig", SmartCardConfig.SMART_CARD);

        MockUp<AdministrationAuthorityRepository> administrationAuthorityRepositoryMockUp =
            new MockUp<AdministrationAuthorityRepository>() {

                @Mock
                public String find(final String id) {
                    return administrationAuthorityJSON;
                }
            };

        Deencapsulation.setField(sut, "administrationAuthorityRepository",
            administrationAuthorityRepositoryMockUp.getMockInstance());

        String SUBJECT_COMMON_NAME = "subject.common.name";
        String SUBJECT_ORGANIZATIONAL_UNIT = "subject.organizational.unit";
        String SUBJECT_ORGANIZATION = "subject.organization";
        String SUBJECT_COUNTRY = "subject.country";
        subject = new X500Principal("C=" + SUBJECT_COUNTRY + ", O=" + SUBJECT_ORGANIZATION + ", OU="
            + SUBJECT_ORGANIZATIONAL_UNIT + ", CN=" + SUBJECT_COMMON_NAME);
        CSRGenerator CSRGenerator = new CSRGenerator();

        KeyPairConverter keyPairConverter = new KeyPairConverter();
        PublicKey publicKey = keyPairConverter.getPublicKeyForEncryptingFromPem(publicKeyForEncryptionPEM);
        PrivateKey privateKey = keyPairConverter.getPrivateKeyForEncryptingFromPem(privateKeyForEncryptionPEM);

        csr = CSRGenerator.generate(publicKey, privateKey, subject);

        admBoardId = "db033b3f729c45719db8aba15d24043c";

        sut.init();

    }

    @Test
    public void constituteTest(@Mocked final CreateSharesHandler createSharesHandlerRSA,
            @Mocked final PKCS10CertificationRequest csr, @Mocked final PemUtils pemUtils,
            @Mocked final CertificateRequestSigner certificateRequestSigner,
            @Mocked final ConfigConstants configConstants)
            throws URISyntaxException, ResourceNotFoundException, SharesException, IOException, ApplicationException {

        String pin = "222222";
        String password = "2222222222222222";
        String hashMember = getHashValueForMember(testMember0);
        String label0 = hashMember.substring(0, Math.min(hashMember.length(), Constants.SMART_CARD_LABEL_MAX_LENGTH));
        hashMember = getHashValueForMember(testMember1);
        String label1 = hashMember.substring(0, Math.min(hashMember.length(), Constants.SMART_CARD_LABEL_MAX_LENGTH));
        Path fullPath = Paths.get(getClass().getResource("/tenant_manolo.sks").toURI()).toAbsolutePath();
        InputStream input = Files.newInputStream(fullPath);
        String pathToGiveToResolver = Paths.get("target/").toAbsolutePath().toString();
        PathResolver initializedPathResolver = new PrefixPathResolver(pathToGiveToResolver);

        MockUp<ConfigurationEntityStatusService> statusServiceMockUp = new MockUp<ConfigurationEntityStatusService>() {

            @Mock
            public String update(final String newStatus, final String id, final EntityRepository repository) {
                return "";
            }
        };

        Deencapsulation.setField(sut, "statusService", statusServiceMockUp.getMockInstance());

        new Expectations() {
            {
                Deencapsulation.setField(ConfigConstants.class, "CONFIG_FILES_BASE_DIR", "target");
                Deencapsulation.setField(sut, "pathResolver", initializedPathResolver);
            }
        };

        sut.constitute(admBoardId, input, password.toCharArray());

        new MockUp<StatelessReadSharesHandler>() {
            @Mock
            public String getSmartcardLabel() {
                return label0;
            }
        };
        sut.writeShare(admBoardId, 0, pin);

        new MockUp<StatelessReadSharesHandler>() {
            @Mock
            public String getSmartcardLabel() {
                return label1;
            }
        };
        sut.writeShare(admBoardId, 1, pin);

        // Current framework does not execute the Runnables, so no file is
        // generated
    }

    @Test
    public void getSmartCardReaderStatusTest() {

        MockUp<SmartCardService> smartcardServiceMockUp = new MockUp<SmartCardService>() {

            @Mock
            public boolean isSmartcardOk() {
                return true;
            }
        };

        SmartCardService mockInstanceSmartcardService = smartcardServiceMockUp.getMockInstance();
        Deencapsulation.setField(sut, "smartcardService", mockInstanceSmartcardService);

        new Expectations() {
            {
                mockInstanceSmartcardService.isSmartcardOk();
            }
        };

        SmartCardStatus smartCardStatus = sut.getSmartCardReaderStatus();

        Assert.assertEquals(SmartCardStatus.INSERTED, smartCardStatus);
    }

    @Test
    public void writeShareTest(@Mocked final CreateSharesHandler createSharesHandler)
            throws ResourceNotFoundException, SharesException, IOException, ApplicationException {

        String adminBoardId = "db033b3f729c45719db8aba15d24043c";
        int shareNumber = 0;
        String pin = "222222";
        String hashMember = getHashValueForMember(testMember0);
        String label = hashMember.substring(0, Math.min(hashMember.length(), Constants.SMART_CARD_LABEL_MAX_LENGTH));

        Map<String, CreateSharesHandler> createSharesHandlerRSAMap = new HashMap<>();
        createSharesHandlerRSAMap.put(adminBoardId, createSharesHandler);

        Deencapsulation.setField(sut, "createSharesHandlerRSAMap", createSharesHandlerRSAMap);
        Deencapsulation.setField(sut, "puk", "222222");

        new Expectations() {
            {
                createSharesHandler.writeShareAndSelfSign(shareNumber, label, pin, pin, (Runnable) any);
            }
        };

        new MockUp<StatelessReadSharesHandler>() {
            @Mock
            public String getSmartcardLabel() {
                return label;
            }
        };

        sut.writeShare(adminBoardId, shareNumber, pin);
    }

    @Test
    public void activateAdminBoard() throws GeneralCryptoLibException {

        String pathToGiveToResolver = Paths.get("src/test/resources/").toAbsolutePath().toString();
        PathResolver initializedPathResolver = new PrefixPathResolver(pathToGiveToResolver);

        Deencapsulation.setField(sut, "pathResolver", initializedPathResolver);

        ActivateOutputData output = sut.activate("dc742be1d49b42ee83cfe1652a8170ac");

        assertThat(output.getIssuerPublicKeyPEM().isEmpty(), is(false));

        PublicKey publicKey =
            com.scytl.cryptolib.primitives.primes.utils.PemUtils.publicKeyFromPem(output.getIssuerPublicKeyPEM());

        assertThat(publicKey != null, is(true));

    }

    @Test
    public void activateAdminBoardShouldThrowExceptionWhenCertificateDoesNotCorrespondToTheID()
            throws GeneralCryptoLibException {

        expected.expect(RuntimeException.class);

        String pathToGiveToResolver = Paths.get("src/test/resources/").toAbsolutePath().toString();
        PathResolver initializedPathResolver = new PrefixPathResolver(pathToGiveToResolver);

        Deencapsulation.setField(sut, "pathResolver", initializedPathResolver);

        sut.activate(admBoardId);
    }

    @Test
    public void readShareTest() throws SharesException, GeneralCryptoLibException, ResourceNotFoundException {

        String pin = "222222";
        String hashMember = getHashValueForMember(testMember0);
        String truncatedHashMember =
            hashMember.substring(0, Math.min(hashMember.length(), Constants.SMART_CARD_LABEL_MAX_LENGTH));

        MockUp<StatelessReadSharesHandler> statelessReader = new MockUp<StatelessReadSharesHandler>() {

            @Mock
            public String readShareAndStringify(final String pin, final PublicKey publicKey) {
                return "";
            }

            @Mock
            public String getSmartcardLabel() {
                return truncatedHashMember;
            }

        };

        StatelessReadSharesHandler statelessReaderMockInstance = statelessReader.getMockInstance();
        Deencapsulation.setField(sut, "statelessReadSharesHandler", statelessReaderMockInstance);

        PublicKey publicKey =
            com.scytl.cryptolib.primitives.primes.utils.PemUtils.publicKeyFromPem(publicKeyForEncryptionPEM);

        new Expectations() {
            {
                statelessReaderMockInstance.readShareAndStringify(pin, publicKey);
            }
        };

        sut.readShare(admBoardId, 0, pin, publicKeyForEncryptionPEM);
    }

    @Test
    public void readShareShouldThrowExceptionWithSmartcardFromAnotherMember()
            throws SharesException, GeneralCryptoLibException, ResourceNotFoundException {

        expected.expect(RuntimeException.class);

        String pin = "222222";

        MockUp<StatelessReadSharesHandler> statelessReader = new MockUp<StatelessReadSharesHandler>() {

            @Mock
            public String readShareAndStringify(final String pin, final PublicKey publicKey) {
                return "";
            }

            @Mock
            public String getSmartcardLabel() {
                return "anotherMember";
            }

        };

        StatelessReadSharesHandler statelessReaderMockInstance = statelessReader.getMockInstance();
        Deencapsulation.setField(sut, "statelessReadSharesHandler", statelessReaderMockInstance);

        PublicKey publicKey =
            com.scytl.cryptolib.primitives.primes.utils.PemUtils.publicKeyFromPem(publicKeyForEncryptionPEM);

        new Expectations() {
            {
                statelessReaderMockInstance.readShareAndStringify(pin, publicKey);
            }
        };

        sut.readShare(admBoardId, 0, pin, publicKeyForEncryptionPEM);
    }

    @Test
    public void reconstructPrivateKey() throws GeneralCryptoLibException, SharesException {

        PrivateKey privateKey =
            com.scytl.cryptolib.primitives.primes.utils.PemUtils.privateKeyFromPem(privateKeyForEncryptionPEM);

        MockUp<StatelessReadSharesHandler> statelessReader = new MockUp<StatelessReadSharesHandler>() {

            @Mock
            public PrivateKey getPrivateKeyWithSerializedShares(final Set<String> shares, final PublicKey publicKey) {
                return privateKey;
            }

        };

        StatelessReadSharesHandler statelessReaderMockInstance = statelessReader.getMockInstance();
        Deencapsulation.setField(sut, "statelessReadSharesHandler", statelessReaderMockInstance);

        String privateKeyPEM = sut.reconstruct(admBoardId, new ArrayList<>(), publicKeyForEncryptionPEM);

        assertThat(privateKeyPEM.replace("\r", "").replace("\n", "").equals(privateKeyForEncryptionPEM), is(true));

    }

    private String getHashValueForMember(final String member) {

        String base64Value = null;

        try {
            MessageDigest mdEnc = MessageDigest.getInstance(Constants.MESSAGE_DIGEST_ALGORITHM);

            byte[] memberByteArray = member.getBytes(StandardCharsets.UTF_8);
            mdEnc.update(memberByteArray, 0, memberByteArray.length);

            base64Value = Base64.getEncoder().encodeToString(mdEnc.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Cryptolib exception when getting hash for member: " + member, e);
        }

        return base64Value;
    }

}
