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

import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.cert.CertificateException;
import java.util.Properties;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.AnnotationConfigContextLoader;

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.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.primitives.service.PrimitivesService;
import com.scytl.cryptolib.scytl.keystore.cryptoapi.CryptoAPIScytlKeyStore;
import com.scytl.cryptolib.scytl.keystore.service.ScytlKeyStoreService;
import com.scytl.products.ov.config.tenant.commands.CommandParameter;
import com.scytl.products.ov.config.tenant.commands.GenerateTenantParametersHolder;
import com.scytl.products.ov.config.tenant.commands.Parameters;
import com.scytl.products.ov.config.tenant.commands.UpdateTenantParametersAdapter;
import com.scytl.products.ov.config.tenant.commands.UpdateTenantParametersHolder;
import com.scytl.products.ov.config.tenant.service.CertificateService;
import com.scytl.products.ov.config.tenant.service.ConfigTenantService;
import com.scytl.products.ov.config.tenant.service.IOUtilsService;
import com.scytl.products.ov.config.tenant.service.KeyPairService;
import com.scytl.products.ov.config.tenant.service.KeyStoreService;
import com.scytl.products.ov.config.tenant.service.ValidationService;
import com.scytl.products.ov.datapacks.beans.CredentialProperties;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class, loader = AnnotationConfigContextLoader.class)
public class ApplicationTest {

    private static Path outputPath;

    private static PrimitivesServiceAPI primitivesServiceAPI;

    private static AsymmetricService asymmetricService;

    private static Path passwordPaths;


    private static Properties passwordProperties;

    private static Path rootCAUpdatePath;

    private static Path tenantCertUpdatePath;

    private static Path tenantKSUpdatePath;

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

    @Autowired
    CertificateService certificateService;

    @Autowired
    KeyStoreService keyStoreService;

    @Autowired
    ValidationService validationService;

    @Autowired
    IOUtilsService ioUtilsService;

    @Autowired
    KeyPairService keyPairService;

    @Autowired
    ConfigTenantService configTenantService;

    @BeforeClass
    public static void setup() throws IOException, URISyntaxException, GeneralCryptoLibException {

        Security.addProvider(new BouncyCastleProvider());

        URL url = Thread.currentThread().getContextClassLoader().getResource("outputPath");
        outputPath = Paths.get(url.toURI());
        Files.createDirectories(outputPath);
        primitivesServiceAPI = new PrimitivesService();
        asymmetricService = new AsymmetricService();
        passwordPaths = Paths.get("testMaterial", "passwords.txt");
        rootCAUpdatePath = Paths.get("testMaterial", "updatetenant", "swissPostRootCA.pem");
        tenantCertUpdatePath = Paths.get("testMaterial", "updatetenant", "tenant_neuchatel.pem");
        tenantKSUpdatePath = Paths.get("testMaterial", "updatetenant", "tenant_neuchatel.sks");
        passwordProperties = readProperties();

    }

    private static Properties readProperties() {

        Properties props = new Properties();

        try (InputStream input =
            Thread.currentThread().getContextClassLoader().getResourceAsStream(passwordPaths.toString())) {
            props.load(input);
        } catch (IOException e) {
            throw new RuntimeException("Error reading the credential properties");
        }
        return props;
    }

    private static Path getCurrentPath(final Path path) throws URISyntaxException {

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        URL resource = classLoader.getResource(path.toString());
        return Paths.get(resource.toURI());

    }

    /**
     * Creates a CSR object and verifies signature on some random string
     *
     * @throws GeneralCryptoLibException
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     */
    @Test
    public void testCSRGeneration()
            throws GeneralCryptoLibException, IOException, NoSuchAlgorithmException, InvalidKeyException {

        String tenantName = "tenantTest";
        KeyPair keyPair = keyPairService.generateKeyPairForSigning();
        CredentialProperties credentialProperties = ioUtilsService.getCredentialProperties();
        final PKCS10CertificationRequest csr =
            certificateService.createCSR(keyPair.getPublic(), keyPair.getPrivate(), credentialProperties, tenantName);

        // generate a random value to verify a signature with the public key
        // inside the CSR with the keyPair
        String randomString = primitivesServiceAPI.get32CharAlphabetCryptoRandomString().nextRandom(20);
        byte[] signature = asymmetricService.sign(keyPair.getPrivate(), randomString.getBytes(StandardCharsets.UTF_8));
        JcaPKCS10CertificationRequest jcaPKCS10CertificationRequest = new JcaPKCS10CertificationRequest(csr);
        assertTrue(asymmetricService.verifySignature(signature, jcaPKCS10CertificationRequest.getPublicKey(),
            randomString.getBytes(StandardCharsets.UTF_8)));

    }

    @Test
    public void generatecCSRWithInvalidKeyPair() {

        String tenantName = "tenantTest";
        expectedException.expect(RuntimeException.class);
        KeyPair keyPair = keyPairService.generateKeyPairForSigning();
        KeyPair keyPair2 = keyPairService.generateKeyPairForSigning();
        CredentialProperties credentialProperties = ioUtilsService.getCredentialProperties();

        final PKCS10CertificationRequest csr =
            certificateService.createCSR(keyPair.getPublic(), keyPair2.getPrivate(), credentialProperties, tenantName);
    }

    @Test
    public void testCertificateGenerated()
            throws GeneralCryptoLibException, IOException, NoSuchAlgorithmException, InvalidKeyException,
            CertificateException, ValidationException {

        String tenantName = "tenantTest";
        KeyPair keyPair = keyPairService.generateKeyPairForSigning();
        CredentialProperties credentialProperties = ioUtilsService.getCredentialProperties();

        CryptoAPIX509Certificate keystoreCertificate =
            certificateService.generateCertificate(keyPair, credentialProperties, tenantName);
        validationService.validateCertificate(keystoreCertificate.getCertificate());

    }

    @Test
    public void testGeneratedKeystore()
            throws GeneralCryptoLibException, NoSuchAlgorithmException, InvalidKeyException, ValidationException {
        String tenantName = "tenantTest";
        KeyPair keyPair = keyPairService.generateKeyPairForSigning();
        CredentialProperties credentialProperties = ioUtilsService.getCredentialProperties();
        final PKCS10CertificationRequest csr =
            certificateService.createCSR(keyPair.getPublic(), keyPair.getPrivate(), credentialProperties, tenantName);

        char[] password = passwordProperties.getProperty("tenant").toCharArray();
        CryptoAPIX509Certificate keystoreCertificate =
            certificateService.generateCertificate(keyPair, credentialProperties, tenantName);
        final CryptoAPIScytlKeyStore tenantKeyStore =
            keyStoreService.createTenantKeyStore(keyPair, credentialProperties, keystoreCertificate, password);
        // generate a random value to verify a signature with the public key
        // inside the CSR with the keyPair
        String randomString = primitivesServiceAPI.get32CharAlphabetCryptoRandomString().nextRandom(20);
        String alias = credentialProperties.getAlias().get(CertificatePropertiesConstants.PRIVATE_KEY_ALIAS);
        byte[] signature = asymmetricService.sign(tenantKeyStore.getPrivateKeyEntry(alias, password),
            randomString.getBytes(StandardCharsets.UTF_8));
        JcaPKCS10CertificationRequest jcaPKCS10CertificationRequest = new JcaPKCS10CertificationRequest(csr);
        assertTrue(asymmetricService.verifySignature(signature, jcaPKCS10CertificationRequest.getPublicKey(),
            randomString.getBytes(StandardCharsets.UTF_8)));

    }

    @Test
    public void testGenerateCSRandKeystore()
            throws GeneralCryptoLibException, NoSuchAlgorithmException, InvalidKeyException, IOException {

        String tenantName = "test";
        passwordProperties.getProperty("tenant").toCharArray();
        String outPutPathString = outputPath.toString();
        GenerateTenantParametersHolder holder = new GenerateTenantParametersHolder(tenantName, outPutPathString);
        configTenantService.generateCSRandKeystore(passwordProperties.getProperty("tenant").toCharArray(), holder);
        Path csrPath = Paths.get(outPutPathString, "tenant_" + tenantName + "_CSR.pem");
        Path keystorePath = Paths.get(outPutPathString, "tenant_" + tenantName + ".sks");
        assertTrue(Files.exists(csrPath));
        assertTrue(Files.exists(keystorePath));

        final ScytlKeyStoreService scytlKeyStoreService = new ScytlKeyStoreService();
        KeyStore.PasswordProtection passwordProtection =
            new KeyStore.PasswordProtection(passwordProperties.getProperty("tenant").toCharArray());
        CryptoAPIScytlKeyStore cryptoScytlKeyStore;

        final JcaPKCS10CertificationRequest csrFromPEM = certificateService.getCSRFromPEM(csrPath);
        final CryptoAPIScytlKeyStore cryptoAPIScytlKeyStore =
            keyStoreService.loadKeyStore(keystorePath, passwordProperties.getProperty("tenant").toCharArray());
        String randomString = primitivesServiceAPI.get32CharAlphabetCryptoRandomString().nextRandom(20);
        CredentialProperties credentialProperties = ioUtilsService.getCredentialProperties();
        String alias = credentialProperties.getAlias().get(CertificatePropertiesConstants.PRIVATE_KEY_ALIAS);
        byte[] signature = asymmetricService.sign(
            cryptoAPIScytlKeyStore.getPrivateKeyEntry(alias, passwordProperties.getProperty("tenant").toCharArray()),
            randomString.getBytes(StandardCharsets.UTF_8));
        assertTrue(asymmetricService.verifySignature(signature, csrFromPEM.getPublicKey(),
            randomString.getBytes(StandardCharsets.UTF_8)));

    }

    @Test
    public void updateKeystore()
            throws URISyntaxException, ValidationException, CertificateException, GeneralCryptoLibException,
            IOException {

        UpdateTenantParametersAdapter adapter = new UpdateTenantParametersAdapter();

        Parameters parameters = new Parameters();
        Path currentPathKeystore = getCurrentPath(tenantKSUpdatePath);
        Path newKeystorePath = Paths.get(outputPath.toString(), "tenant_neuchatel.sks");
        Files.copy(currentPathKeystore, newKeystorePath, StandardCopyOption.REPLACE_EXISTING);

        parameters.addParam(CommandParameter.PLATFORM_ROOT_CA.getParameterName(), getCurrentPath(rootCAUpdatePath));
        parameters.addParam(CommandParameter.TENANT_CERTIFICATE.getParameterName(),
            getCurrentPath(tenantCertUpdatePath));
        parameters.addParam(CommandParameter.KEYSTORE_PATH.getParameterName(), newKeystorePath);
        final UpdateTenantParametersHolder updateTenantParametersHolder = adapter.adapt(parameters);
        configTenantService.updateTenantKeystore(passwordProperties.getProperty("tenant2").toCharArray(),
            updateTenantParametersHolder);

    }

    @Test
    public void updateKeystoreInvalidPassword()
            throws URISyntaxException, ValidationException, CertificateException, GeneralCryptoLibException,
            IOException {
        UpdateTenantParametersAdapter adapter = new UpdateTenantParametersAdapter();
        expectedException.expect(RuntimeException.class);
        expectedException.expectMessage("An error occurred updating the keystore");
        Parameters parameters = new Parameters();
        Path currentPathKeystore = getCurrentPath(tenantKSUpdatePath);
        Path newKeytorePath = Paths.get(outputPath.toString(), "tenant_neuchatel.sks");
        Files.copy(currentPathKeystore, newKeytorePath, StandardCopyOption.REPLACE_EXISTING);

        parameters.addParam(CommandParameter.PLATFORM_ROOT_CA.getParameterName(), getCurrentPath(rootCAUpdatePath));
        parameters.addParam(CommandParameter.TENANT_CERTIFICATE.getParameterName(),
            getCurrentPath(tenantCertUpdatePath));
        parameters.addParam(CommandParameter.KEYSTORE_PATH.getParameterName(), newKeytorePath);
        final UpdateTenantParametersHolder updateTenantParametersHolder = adapter.adapt(parameters);
        configTenantService.updateTenantKeystore("654654654654654654654".toCharArray(), updateTenantParametersHolder);
    }

}
