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

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

import com.scytl.cryptolib.scytl.keystore.service.ScytlKeyStoreService;
import com.scytl.products.ov.config.io.ServicesInput;
import com.scytl.products.ov.constants.CertificatePropertiesConstants;
import com.scytl.products.ov.datapacks.beans.CertificateParameters;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Test;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.certificates.bean.X509CertificateType;
import com.scytl.cryptolib.certificates.factory.CryptoX509Certificate;
import com.scytl.cryptolib.primitives.primes.utils.PemUtils;
import com.scytl.cryptolib.primitives.service.PrimitivesService;
import com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore;
import com.scytl.products.ov.config.utils.ChainValidator;
import com.scytl.products.ov.config.utils.KeyPairValidator;
import com.scytl.products.ov.constants.Constants;
import com.scytl.products.ov.utils.ConfigObjectMapper;
import com.scytl.products.ov.utils.KeyStoreLoader;
import com.scytl.products.ov.utils.PasswordReaderUtils;

import mockit.Mock;
import mockit.MockUp;

public class GenerateSystemKeysCommandTest {

    public static final String OUTPUT_PATH = "target/systemKeys/";

    public static final char[] ROOT_PASSWORD = "1111111111111111".toCharArray();

    private static final String PRIVATE_KEY_ALIAS = "privatekey";

    private static final String TENANT_NAME = "manolo";

    private static final String ROOT_KS_PATH = "src/test/resources/testMaterial/platformRoot/swisspostRootCA.sks";

    private static final String SERVICES_JSON = "src/test/resources/serviceCertificateParameters.json";

    private static Map<String, String> params2values;

    private static KeyPairValidator keyPairValidator;

    private GenerateSystemKeysCommand _sut;

    @BeforeClass
    public static void setUp() {

        Security.addProvider(new BouncyCastleProvider());

        String[] inputParams =
            {"-out", OUTPUT_PATH, "-ks", ROOT_KS_PATH, "-ser", SERVICES_JSON, "-tenant", TENANT_NAME };
        params2values = Action.GEN_SYSTEM_KEYS.validateAndArrange(inputParams);

        keyPairValidator = new KeyPairValidator();

    }

    @Test
    public void generateSystemKeys()
            throws IOException, GeneralCryptoLibException, UnrecoverableKeyException, CertificateException,
            NoSuchAlgorithmException, KeyStoreException {

        _sut = new GenerateSystemKeysCommand(new PrimitivesService(), new ScytlKeyStoreService());

        new MockUp<PasswordReaderUtils>() {

            @Mock
            public char[] readPasswordFromConsole() {
                return "1111111111111111".toCharArray();
            }
        };

        Map<String, String> service2passwords;

        try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(baos);) {
            PrintStream old = System.out;
            System.setOut(ps);

            _sut.execute(params2values);
            ps.flush();
            System.setOut(old);

            System.out.println(baos.toString());
            service2passwords = getPasswords(baos);
        }

        ServicesInput servicesInput = getServiceCertificatePropertiesFromJson(Paths.get(SERVICES_JSON));
        JsonServiceNamesProvider serviceNamesProvider = new JsonServiceNamesProvider();

        CryptoAPIScytlKeyStore rootKeyStore =
            KeyStoreLoader.loadPlatformRootKeyStore(Paths.get(ROOT_KS_PATH), ROOT_PASSWORD);
        Certificate platformRootCA = rootKeyStore.getCertificateChain(PRIVATE_KEY_ALIAS)[0];

        servicesInput.getServiceCertificateParameters().forEach((serviceName, purposes) -> {

            String encPassword = service2passwords.get(TENANT_NAME + "_" + serviceName);

            purposes.forEach(purpose -> {
                try {
                    validateSystemKeys(serviceName, encPassword.toCharArray(), platformRootCA,
                            purpose);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });
        });

    }

    private Map<String, String> getPasswords(final ByteArrayOutputStream baos) {

        Map<String, String> map = new HashMap<>();

        String[] lines = baos.toString().split(System.lineSeparator());

        for (int i = 0; i < lines.length; i++) {
            String[] splitLine = lines[i].split(Constants.EQUAL);
            if (splitLine.length == 2) {
                map.put(splitLine[0], splitLine[1]);
            }
        }

        return map;
    }

    private void validateSystemKeys(final String service, final char[] password,
            final Certificate platformRootCA, CertificateParameters.Type type)
            throws IOException, GeneralCryptoLibException, CertificateException, KeyStoreException,
            NoSuchAlgorithmException, UnrecoverableKeyException {

        final Path pathCertificate = Paths.get(OUTPUT_PATH, TENANT_NAME + "_" + service + "_" + type.name() + Constants.PEM);
        Certificate certificate =
            PemUtils.certificateFromPem(new String(Files.readAllBytes(pathCertificate), StandardCharsets.UTF_8));

        CryptoX509Certificate cryptoCert = new CryptoX509Certificate((X509Certificate) certificate);

        assertThat(cryptoCert.getSubjectDn().getCommonName().contains(TENANT_NAME), is(true));
        assertThat(cryptoCert.getSubjectDn().getCommonName().contains(service), is(true));

        ChainValidator.validateChain(platformRootCA, certificate, getX509CertificateType(type));
        PublicKey publicKey = certificate.getPublicKey();

        final Path pathKeyStore = Paths.get(OUTPUT_PATH, TENANT_NAME + "_" + service + Constants.SKS);

        CryptoAPIScytlKeyStore keyStore = KeyStoreLoader.loadPlatformRootKeyStore(pathKeyStore, password);

        String alias = GenerateSystemKeysCommand.getCredentialProperties(type).getAlias()
                .get(CertificatePropertiesConstants.PRIVATE_KEY_ALIAS);
        Certificate[] certificates = keyStore.getCertificateChain(alias);
        ChainValidator.validateChain(certificates[1], certificates[0], getX509CertificateType(type));

        PrivateKey privateKey = keyStore.getPrivateKeyEntry(alias, password);

        keyPairValidator.validateKeyPair(publicKey, privateKey);
    }

    /**
     * Get the X.509 equivalent of an internal certificate type.
     *
     * @param type the internal certificate type
     * @return the X.509 certificate type
     */
    private X509CertificateType getX509CertificateType(CertificateParameters.Type type) {
        switch (type) {
        case SIGN:
            return X509CertificateType.SIGN;
        case ENCRYPTION:
            return X509CertificateType.ENCRYPT;
        default:
            throw new IllegalArgumentException("Unsupported type");
        }
    }

    private ServicesInput getServiceCertificatePropertiesFromJson(final Path pathToJson) {

        ConfigObjectMapper _configObjectMapper = new ConfigObjectMapper();
        ServicesInput servicesInput = null;
        try {
            servicesInput = _configObjectMapper.fromJSONFileToJava(pathToJson.toFile(), ServicesInput.class);
        } catch (IOException e) {
            throw new RuntimeException("An error occurred while loading the service certificate properties file", e);
        }

        return servicesInput;
    }
}
