/*
 * 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.cryptolib.certificates.utils;

import static junitparams.JUnitParamsRunner.$;

import java.security.KeyPair;
import java.security.PublicKey;
import java.security.Security;
import java.util.Date;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.AfterClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.asymmetric.utils.AsymmetricTestDataGenerator;
import com.scytl.cryptolib.certificates.bean.X509CertificateType;
import com.scytl.cryptolib.certificates.bean.X509DistinguishedName;
import com.scytl.cryptolib.certificates.configuration.X509CertificateValidationData;
import com.scytl.cryptolib.certificates.configuration.X509CertificateValidationResult;
import com.scytl.cryptolib.certificates.configuration.X509CertificateValidationType;
import com.scytl.cryptolib.certificates.cryptoapi.CryptoAPIX509Certificate;
import com.scytl.cryptolib.certificates.factory.CryptoX509Certificate;
import com.scytl.cryptolib.test.tools.bean.TestPublicKey;

import junitparams.JUnitParamsRunner;
import junitparams.Parameters;

/**
 * Tests of X509 certificate validator input validation.
 */
@RunWith(JUnitParamsRunner.class)
public class X509CertificateValidatorValidationTest {

    private static KeyPair _rootKeyPair;

    private static PublicKey _rootPublicKey;

    private static KeyPair _keyPair;

    private static PublicKey _publicKey;

    private static X509DistinguishedName _rootSubjectDn;

    private static X509DistinguishedName _subjectDn;

    private static Date _validDate;

    static {
        try {
            setUp();
        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException(
                "Setup failed for class "
                    + X509CertificateValidatorValidationTest.class
                        .getSimpleName());
        }
    }

    public static void setUp() throws GeneralCryptoLibException {

        Security.addProvider(new BouncyCastleProvider());

        _rootKeyPair = AsymmetricTestDataGenerator.getKeyPairForSigning();
        _rootPublicKey = _rootKeyPair.getPublic();
        _rootSubjectDn =
            X509CertificateTestDataGenerator.getRootDistinguishedName();

        _keyPair = AsymmetricTestDataGenerator.getKeyPairForSigning();
        _publicKey = _keyPair.getPublic();
        _subjectDn =
            X509CertificateTestDataGenerator.getDistinguishedName();

        _validDate =
            X509CertificateTestDataGenerator.getDateWithinValidityPeriod();
    }

    @AfterClass
    public static void tearDown() {

        Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
    }

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

    @Test
    @Parameters(method = "createX509CertificateValidationData")
    public void testX509CertificateValidationDataCreationValidation(
            Date date, X509DistinguishedName subjectDn,
            X509DistinguishedName issuerDn,
            X509CertificateType certificateType,
            PublicKey issuerPublicKey, String errorMsg)
            throws GeneralCryptoLibException {

        thrown.expect(GeneralCryptoLibException.class);
        thrown.expectMessage(errorMsg);
        new X509CertificateValidationData.Builder().addDate(date)
            .addSubjectDn(subjectDn).addIssuerDn(issuerDn)
            .addKeyType(certificateType).addCaPublicKey(issuerPublicKey)
            .build();
    }

    public static Object[] createX509CertificateValidationData() {

        return $(
            $(null, _subjectDn, _rootSubjectDn, X509CertificateType.SIGN,
                _rootPublicKey, "Date is null."),
            $(_validDate, null, _rootSubjectDn, X509CertificateType.SIGN,
                _rootPublicKey, "Subject distinguished name is null."),
            $(_validDate, _subjectDn, null, X509CertificateType.SIGN,
                _rootPublicKey, "Issuer distinguished name is null."),
            $(_validDate, _subjectDn, _rootSubjectDn, null,
                _rootPublicKey, "Certificate type is null."),
            $(_validDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.SIGN, null,
                "Issuer public key is null."),
            $(_validDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.SIGN, new TestPublicKey(null),
                "Issuer public key content is null."),
            $(_validDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.SIGN, new TestPublicKey(new byte[0]),
                "Issuer public key content is empty."));
    }

    @Test
    @Parameters(method = "createX509CertificateValidator")
    public void testX509CertificateValidatorCreationValidation(
            final CryptoX509Certificate cryptoX509Cert,
            final X509CertificateValidationData validationData,
            final X509CertificateValidationType[] validationTypes,
            String errorMsg) throws GeneralCryptoLibException {

        thrown.expect(GeneralCryptoLibException.class);
        thrown.expectMessage(errorMsg);
        new X509CertificateValidator(cryptoX509Cert, validationData,
            validationTypes);
    }

    public static Object[] createX509CertificateValidator()
            throws GeneralCryptoLibException {

        CryptoAPIX509Certificate certificate =
            X509CertificateTestDataGenerator.getSignX509Certificate(
                _keyPair, _rootKeyPair);

        X509CertificateValidationData validationData =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.SIGN, _rootPublicKey);

        X509CertificateValidationType[] validationTypes =
            X509CertificateTestDataGenerator
                .getCertificateValidationTypes();

        X509CertificateValidationType[] validationTypesWithNullValue =
            X509CertificateTestDataGenerator
                .getCertificateValidationTypes();
        validationTypesWithNullValue[0] = null;

        return $(
            $(null, validationData, validationTypes,
                "Certificate to validate is null."),
            $(certificate, null, validationTypes,
                "Validation data is null."),
            $(certificate, validationData, null,
                "Validation type array is null."),
            $(certificate, validationData,
                new X509CertificateValidationType[0],
                "Validation type array is empty."),
            $(certificate, validationData, validationTypesWithNullValue,
                "A validation type is null."));
    }

    @Test
    @Parameters(method = "validateRootAuthorityCertificate")
    public void testRootAuthorityCertificateValidation(
            final CryptoX509Certificate certificate,
            final X509CertificateValidationData validationData,
            final X509CertificateValidationType failedValidationType,
            String errorMsg) throws GeneralCryptoLibException {

        thrown.expect(GeneralCryptoLibException.class);
        thrown.expectMessage(errorMsg);
        validateCertificate(certificate, validationData,
            failedValidationType);
    }

    public static Object[] validateRootAuthorityCertificate()
            throws GeneralCryptoLibException {

        CryptoAPIX509Certificate rootCertificate =
            X509CertificateTestDataGenerator
                .getRootAuthorityX509Certificate(_rootKeyPair);

        Date invalidDate =
            X509CertificateTestDataGenerator
                .getDateOutsideValidityPeriod();
        X509CertificateValidationData validationDataWithInvalidDate =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                invalidDate, _rootSubjectDn, _rootSubjectDn,
                X509CertificateType.CERTIFICATE_AUTHORITY, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidSubject =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.CERTIFICATE_AUTHORITY, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidIssuer =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _rootSubjectDn, _subjectDn,
                X509CertificateType.CERTIFICATE_AUTHORITY, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidKeyUsage =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _rootSubjectDn, _rootSubjectDn,
                X509CertificateType.SIGN, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidPublicKey =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _rootSubjectDn, _rootSubjectDn,
                X509CertificateType.CERTIFICATE_AUTHORITY, _publicKey);

        return $(
            $(rootCertificate, validationDataWithInvalidDate,
                X509CertificateValidationType.DATE,
                "Validation failed for "
                    + X509CertificateValidationType.DATE.name()),
            $(rootCertificate, validationDataWithInvalidSubject,
                X509CertificateValidationType.SUBJECT,
                "Validation failed for "
                    + X509CertificateValidationType.SUBJECT.name()),
            $(rootCertificate, validationDataWithInvalidIssuer,
                X509CertificateValidationType.ISSUER,
                "Validation failed for "
                    + X509CertificateValidationType.ISSUER.name()),
            $(rootCertificate, validationDataWithInvalidKeyUsage,
                X509CertificateValidationType.KEY_TYPE,
                "Validation failed for "
                    + X509CertificateValidationType.KEY_TYPE.name()),
            $(rootCertificate, validationDataWithInvalidPublicKey,
                X509CertificateValidationType.SIGNATURE,
                "Validation failed for "
                    + X509CertificateValidationType.SIGNATURE.name()));
    }

    @Test
    @Parameters(method = "validateIntermediateAuthorityCertificate")
    public void testIntermediateAuthorityCertificateValidation(
            final CryptoX509Certificate certificate,
            final X509CertificateValidationData validationData,
            final X509CertificateValidationType failedValidationType,
            String errorMsg) throws GeneralCryptoLibException {

        thrown.expect(GeneralCryptoLibException.class);
        thrown.expectMessage(errorMsg);
        validateCertificate(certificate, validationData,
            failedValidationType);
    }

    public static Object[] validateIntermediateAuthorityCertificate()
            throws GeneralCryptoLibException {

        CryptoAPIX509Certificate intermediateCertificate =
            X509CertificateTestDataGenerator
                .getIntermediateAuthorityX509Certificate(_keyPair,
                    _rootKeyPair);

        Date invalidDate =
            X509CertificateTestDataGenerator
                .getDateOutsideValidityPeriod();
        X509CertificateValidationData validationDataWithInvalidDate =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                invalidDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.CERTIFICATE_AUTHORITY, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidSubject =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _rootSubjectDn, _rootSubjectDn,
                X509CertificateType.CERTIFICATE_AUTHORITY, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidIssuer =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _subjectDn, _subjectDn,
                X509CertificateType.CERTIFICATE_AUTHORITY, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidKeyUsage =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.SIGN, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidPublicKey =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.CERTIFICATE_AUTHORITY, _publicKey);

        return $(
            $(intermediateCertificate, validationDataWithInvalidDate,
                X509CertificateValidationType.DATE,
                "Validation failed for "
                    + X509CertificateValidationType.DATE.name()),
            $(intermediateCertificate, validationDataWithInvalidSubject,
                X509CertificateValidationType.SUBJECT,
                "Validation failed for "
                    + X509CertificateValidationType.SUBJECT.name()),
            $(intermediateCertificate, validationDataWithInvalidIssuer,
                X509CertificateValidationType.ISSUER,
                "Validation failed for "
                    + X509CertificateValidationType.ISSUER.name()),
            $(intermediateCertificate, validationDataWithInvalidKeyUsage,
                X509CertificateValidationType.KEY_TYPE,
                "Validation failed for "
                    + X509CertificateValidationType.KEY_TYPE.name()),
            $(intermediateCertificate, validationDataWithInvalidPublicKey,
                X509CertificateValidationType.SIGNATURE,
                "Validation failed for "
                    + X509CertificateValidationType.SIGNATURE.name()));
    }

    @Test
    @Parameters(method = "validateSignCertificate")
    public void testSignCertificateValidation(
            final CryptoX509Certificate certificate,
            final X509CertificateValidationData validationData,
            final X509CertificateValidationType failedValidationType,
            String errorMsg) throws GeneralCryptoLibException {

        thrown.expect(GeneralCryptoLibException.class);
        thrown.expectMessage(errorMsg);
        validateCertificate(certificate, validationData,
            failedValidationType);
    }

    public static Object[] validateSignCertificate()
            throws GeneralCryptoLibException {

        CryptoAPIX509Certificate signCertificate =
            X509CertificateTestDataGenerator.getSignX509Certificate(
                _keyPair, _rootKeyPair);

        Date invalidDate =
            X509CertificateTestDataGenerator
                .getDateOutsideValidityPeriod();
        X509CertificateValidationData validationDataWithInvalidDate =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                invalidDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.SIGN, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidSubject =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _rootSubjectDn, _rootSubjectDn,
                X509CertificateType.SIGN, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidIssuer =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _subjectDn, _subjectDn,
                X509CertificateType.SIGN, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidKeyUsage =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.CERTIFICATE_AUTHORITY, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidPublicKey =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.SIGN, _publicKey);

        return $(
            $(signCertificate, validationDataWithInvalidDate,
                X509CertificateValidationType.DATE,
                "Validation failed for "
                    + X509CertificateValidationType.DATE.name()),
            $(signCertificate, validationDataWithInvalidSubject,
                X509CertificateValidationType.SUBJECT,
                "Validation failed for "
                    + X509CertificateValidationType.SUBJECT.name()),
            $(signCertificate, validationDataWithInvalidIssuer,
                X509CertificateValidationType.ISSUER,
                "Validation failed for "
                    + X509CertificateValidationType.ISSUER.name()),
            $(signCertificate, validationDataWithInvalidKeyUsage,
                X509CertificateValidationType.KEY_TYPE,
                "Validation failed for "
                    + X509CertificateValidationType.KEY_TYPE.name()),
            $(signCertificate, validationDataWithInvalidPublicKey,
                X509CertificateValidationType.SIGNATURE,
                "Validation failed for "
                    + X509CertificateValidationType.SIGNATURE.name()));
    }

    @Test
    @Parameters(method = "validateEncryptionCertificate")
    public void testEncryptionCertificateValidation(
            final CryptoX509Certificate certificate,
            final X509CertificateValidationData validationData,
            final X509CertificateValidationType failedValidationType,
            String errorMsg) throws GeneralCryptoLibException {

        thrown.expect(GeneralCryptoLibException.class);
        thrown.expectMessage(errorMsg);
        validateCertificate(certificate, validationData,
            failedValidationType);
    }

    public static Object[] validateEncryptionCertificate()
            throws GeneralCryptoLibException {

        CryptoAPIX509Certificate encryptionCertificate =
            X509CertificateTestDataGenerator.getEncryptionX509Certificate(
                _keyPair, _rootKeyPair);

        Date invalidDate =
            X509CertificateTestDataGenerator
                .getDateOutsideValidityPeriod();
        X509CertificateValidationData validationDataWithInvalidDate =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                invalidDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.ENCRYPT, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidSubject =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _rootSubjectDn, _rootSubjectDn,
                X509CertificateType.ENCRYPT, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidIssuer =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _subjectDn, _subjectDn,
                X509CertificateType.SIGN, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidKeyUsage =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.CERTIFICATE_AUTHORITY, _rootPublicKey);

        X509CertificateValidationData validationDataWithInvalidPublicKey =
            X509CertificateTestDataGenerator.getCertificateValidationData(
                _validDate, _subjectDn, _rootSubjectDn,
                X509CertificateType.ENCRYPT, _publicKey);

        return $(
            $(encryptionCertificate, validationDataWithInvalidDate,
                X509CertificateValidationType.DATE,
                "Validation failed for "
                    + X509CertificateValidationType.DATE.name()),
            $(encryptionCertificate, validationDataWithInvalidSubject,
                X509CertificateValidationType.SUBJECT,
                "Validation failed for "
                    + X509CertificateValidationType.SUBJECT.name()),
            $(encryptionCertificate, validationDataWithInvalidIssuer,
                X509CertificateValidationType.ISSUER,
                "Validation failed for "
                    + X509CertificateValidationType.ISSUER.name()),
            $(encryptionCertificate, validationDataWithInvalidKeyUsage,
                X509CertificateValidationType.KEY_TYPE,
                "Validation failed for "
                    + X509CertificateValidationType.KEY_TYPE.name()),
            $(encryptionCertificate, validationDataWithInvalidPublicKey,
                X509CertificateValidationType.SIGNATURE,
                "Validation failed for "
                    + X509CertificateValidationType.SIGNATURE.name()));
    }

    private static void validateCertificate(
            final CryptoX509Certificate certificate,
            final X509CertificateValidationData validationData,
            final X509CertificateValidationType failedValidationType)
            throws GeneralCryptoLibException {

        X509CertificateValidator validator =
            new X509CertificateValidator(certificate, validationData,
                failedValidationType);

        X509CertificateValidationResult validationResult =
            validator.validate();

        if (!validationResult.isValidated()
            && validationResult.getFailedValidationTypes().size() == 1
            && (validationResult.getFailedValidationTypes().get(0) == failedValidationType)) {
            throw new GeneralCryptoLibException("Validation failed for "
                + failedValidationType.name());
        }
    }
}
