/*
 * Copyright (c) eHealth
 */
package be.fgov.ehealth.technicalconnector.ra;

import be.ehealth.technicalconnector.exception.TechnicalConnectorException;
import be.ehealth.technicalconnector.service.sts.security.Credential;
import be.ehealth.technicalconnector.service.sts.security.KeyStoreInfo;
import be.ehealth.technicalconnector.service.sts.security.impl.KeyStoreCredential;
import be.ehealth.technicalconnector.utils.IdentifierType;
import be.fgov.ehealth.certra.core.v2.CertificateInfoType;
import be.fgov.ehealth.certra.core.v2.RevocationReasonType;
import be.fgov.ehealth.certra.protocol.v2.GetGenericOrganizationTypesResponse;
import be.fgov.ehealth.technicalconnector.ra.builders.BuilderFactory;
import be.fgov.ehealth.technicalconnector.ra.domain.ActorQualities;
import be.fgov.ehealth.technicalconnector.ra.domain.Certificate;
import be.fgov.ehealth.technicalconnector.ra.domain.ContactData;
import be.fgov.ehealth.technicalconnector.ra.domain.ContractRequest;
import be.fgov.ehealth.technicalconnector.ra.domain.DistinguishedName;
import be.fgov.ehealth.technicalconnector.ra.domain.GeneratedContract;
import be.fgov.ehealth.technicalconnector.ra.domain.GeneratedRevocationContract;
import be.fgov.ehealth.technicalconnector.ra.domain.NewCertificateContract;
import be.fgov.ehealth.technicalconnector.ra.domain.Organization;
import be.fgov.ehealth.technicalconnector.ra.domain.Result;
import be.fgov.ehealth.technicalconnector.ra.domain.RevocationContractRequest;
import be.fgov.ehealth.technicalconnector.ra.domain.RevocationRequest;
import be.fgov.ehealth.technicalconnector.ra.domain.SubmitCSRForForeignerResponseInfo;
import be.fgov.ehealth.technicalconnector.ra.enumaration.Status;
import be.fgov.ehealth.technicalconnector.ra.enumaration.UsageType;
import be.fgov.ehealth.technicalconnector.ra.exceptions.RaException;
import be.fgov.ehealth.technicalconnector.ra.service.AuthenticationCertificateRegistrationService;
import be.fgov.ehealth.technicalconnector.ra.service.EncryptionTokenRegistrationService;
import be.fgov.ehealth.technicalconnector.ra.service.ServiceFactory;
import be.fgov.ehealth.technicalconnector.ra.utils.CertificateUtils;
import be.fgov.ehealth.technicalconnector.ra.utils.KeyStoreManager;
import be.fgov.ehealth.technicalconnector.tests.junit.rule.SessionRule;
import be.fgov.ehealth.technicalconnector.tests.utils.AssumeTools;
import be.fgov.ehealth.technicalconnector.tests.utils.TestPropertiesLoader;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;

import java.net.URL;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.UUID;

import static be.fgov.ehealth.technicalconnector.ra.service.ServiceFactory.getAuthenticationCertificateRegistrationService;
import static be.fgov.ehealth.technicalconnector.ra.service.ServiceFactory.getEncryptionTokenRegistrationService;


/**
 * Integration test class to show how the RA module works
 *
 * @author eHealth Platform
 */
public class RaWalkThroughIntegrationTest {

    @Rule
    public SessionRule rule = SessionRule.withInactiveSession()
            .build();


    @Test
    public void getActorQualities() throws Exception {
        AssumeTools.isUser(get("test.ra.organization.user.ssin"));

        try {
            ActorQualities actorQualities = getAuthenticationCertificateRegistrationService().getActorQualities()
                    .getResult();
            Assert.assertNotNull(actorQualities);
        } catch (RaException e) {
            Assert.fail(e.getErrorCodes()
                    .toString());
        }
    }


    @Test
    public void listOrganization() throws Exception {
        AssumeTools.isUser(get("test.ra.organization.user.ssin"));

        try {
            GetGenericOrganizationTypesResponse response = getAuthenticationCertificateRegistrationService().getOrganizationList()
                    .getResult();
            Assert.assertNotNull(response.getOrganizationTypes());
        } catch (RaException e) {
            Assert.fail(e.getErrorCodes()
                    .toString());
        }
    }

    @Test
    public void listApplicationIds() throws Exception {
        Organization organization = new Organization("0809394427", IdentifierType.CBE, "EHP");

        try {
            List<String> applicationIdList = getAuthenticationCertificateRegistrationService().getApplicationIdList(organization)
                    .getResult();
            Assert.assertNotNull(applicationIdList);
        } catch (RaException e) {
            Assert.fail(e.getErrorCodes()
                    .toString());
        }
    }

    @Test
    public void walkEID() throws Exception {
        try {
            KeyPair authenticationKeyPair = CertificateUtils.generateKeyPair();

            // Generate contract
            ContractRequest generateContractRequest = BuilderFactory.newContractRequestBuilder()
                    .create()
                    .withEid()
                    .withContact(new ContactData(get("test.ra.private.phone"), get("test.ra.private.mail")))
                    .build();
            GeneratedContract generatedContract = getAuthenticationCertificateRegistrationService().generateContract(generateContractRequest)
                    .getResult();

            // Contract must be viewed
            generatedContract.setContractViewed(true);

            walk(new NewCertificateContract(generatedContract, authenticationKeyPair, Arrays.asList(UsageType.TIME_STAMPING)), authenticationKeyPair, new DistinguishedName());
        } catch (RaException e) {
            Assert.fail(e.getErrorCodes()
                    .toString());
        }
    }


    @Test
    public void walkOrganization() throws Exception {
        AssumeTools.isUser(get("test.ra.organization.user.ssin"));

        KeyPair authenticationKeyPair = CertificateUtils.generateKeyPair();

        // Generate contract
        String appplicationId = UUID.randomUUID()
                .toString()
                .replace("-", "")
                .substring(0, 30);
        ContractRequest generateContractRequest = BuilderFactory.newContractRequestBuilder()
                .create()
                .forOrganization()
                .withId(get("test.ra.organization.id"), IdentifierType.NIHII_LABO)
                .withName(get("test.ra.organization.name"))
                .withApplicationId(UUID.randomUUID()
                        .toString()
                        .replace("-", "")
                        .substring(0, 30))
                .forContact()
                .withPrivatePhone(get("test.ra.private.phone"))
                .withPrivateEmail(get("test.ra.private.mail"))
                .withGeneralPhone(get("test.ra.general.phone"))
                .withGeneralEmail(get("test.ra.general.mail"))
                .build();
        GeneratedContract generatedContract = getAuthenticationCertificateRegistrationService().generateContract(generateContractRequest)
                .getResult();

        // Contract must be viewed
        generatedContract.setContractViewed(true);

        // this dn object is just for the purpose of conveniently create the keystore filename used for this test.
        DistinguishedName distinguishedName = new DistinguishedName(new Organization(get("test.ra.organization.id"), IdentifierType.NIHII_LABO, get("test.ra.organization.name")), appplicationId);

        walk(new NewCertificateContract(generatedContract, authenticationKeyPair, Arrays.asList(UsageType.TIME_STAMPING)), authenticationKeyPair, distinguishedName);
    }

    @Test
    public void walkOrganizationWithoutApplicationId() throws Exception {
        AssumeTools.isUser(get("test.ra.organization.user.ssin"));

        KeyPair authenticationKeyPair = CertificateUtils.generateKeyPair();

        // Generate contract
        String appplicationId = UUID.randomUUID()
                .toString()
                .replace("-", "")
                .substring(0, 30);
        Organization organization = new Organization(get("test.ra.organization.id"), IdentifierType.NIHII_PHARMACY, get("test.ra.organization.name"));
        ContractRequest generateContractRequest = BuilderFactory.newContractRequestBuilder()
                .create()
                .forOrganization(organization)
                .withoutApplicationId()
                .withContact(new ContactData(get("test.ra.private.phone"), get("test.ra.private.mail")))
                .build();
        GeneratedContract generatedContract = getAuthenticationCertificateRegistrationService().generateContract(generateContractRequest)
                .getResult();

        // Contract must be viewed
        generatedContract.setContractViewed(true);

        // this dn object is just for the purpose of coveniently create the keystore filename used for this test.
        DistinguishedName distinguishedName = new DistinguishedName(organization, appplicationId);

        walk(new NewCertificateContract(generatedContract, authenticationKeyPair, Arrays.asList(UsageType.TIME_STAMPING)), authenticationKeyPair, distinguishedName);
    }

    @Test
    public void replace() throws Exception {
        try {
            Properties props = TestPropertiesLoader.getProperties("/be.ehealth.technicalconnector.test.properties");
            String eHealthCertificate = props.getProperty("test.keystore.location");
            String eHealthCertificatePwd = props.getProperty("test.keystore.password");
            String alias = props.getProperty("test.keystore.alias");
            KeyStoreInfo ksInfo = new KeyStoreInfo(eHealthCertificate, eHealthCertificatePwd.toCharArray(), alias, eHealthCertificatePwd.toCharArray());
            Credential credential = new KeyStoreCredential(ksInfo);

            //Credential credential = new KeyStoreCredential("Your keystore file name", "authentication", "your password");
            CertificateInfoType certificateInfoType = getAuthenticationCertificateRegistrationService().getCertificateInfoForAuthenticationCertificate(credential)
                    .getResult();
            if (certificateInfoType.isReplaceable()) {
                walkEID();
                EncryptionTokenRegistrationService etkRA = ServiceFactory.getEncryptionTokenRegistrationService();
                etkRA.activateToken(credential);
            }
        } catch (RaException e) {
            List<String> errorCodes = e.getErrorCodes();
            Assert.fail(errorCodes.toString());
        }
    }

    @Test
    public void revoke() throws Exception {
        try {
            AuthenticationCertificateRegistrationService certRA = getAuthenticationCertificateRegistrationService();
            List<CertificateInfoType> certificateInfoTypes = certRA.getCertificateInfoForCitizen()
                    .getResult();
            CertificateInfoType toRevoke = null;
            for (CertificateInfoType certificateInfoType : certificateInfoTypes) {
                if (certificateInfoType.isRevocable()) {
                    toRevoke = certificateInfoType;
                    break;
                }
            }
            if (toRevoke != null) {
                RevocationContractRequest revocationContractRequest = BuilderFactory.newRevokeRequestBuilder()
                        .create()
                        .withPublicKeyIdentifier(toRevoke.getPublicKeyIdentifier())
                        .withRevocationReasonType(RevocationReasonType.KEY_COMPROMISE)
                        .build();
                GeneratedRevocationContract generatedRevocationContract = certRA.generateRevocationContract(revocationContractRequest)
                        .getResult();
                generatedRevocationContract.setContractViewed(true);
                certRA.revokeCertificate(new RevocationRequest(toRevoke.getPublicKeyIdentifier(), generatedRevocationContract));
            }
        } catch (RaException e) {
            Assert.fail(e.getErrorCodes()
                    .toString());
        }
    }


    public void walk(NewCertificateContract contract, KeyPair authenticationKeyPair, DistinguishedName distinguishedName) throws Exception {
        AuthenticationCertificateRegistrationService certRA = getAuthenticationCertificateRegistrationService();
        EncryptionTokenRegistrationService etkRa = getEncryptionTokenRegistrationService();

        String password = "15";
        char[] passwd = "15".toCharArray();

        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        // Set the test.KEYSTORE_LOC.classpath property as the keystore location in your test classpath
        // e.g.: test.KEYSTORE_LOC.classpath=P12/acc/
        URL url = classLoader.getResource(get("test.KEYSTORE_LOC.classpath"));

        String storeFilename = distinguishedName.asNormalisedBaseFileName();
        String storeLocation = url.toURI()
                .getPath() + storeFilename;


        // generate certificate
        Result<Certificate> generateCertificateResponse = certRA.generateCertificate(contract);

        // get certificate
        X509Certificate[] result = pollToGetCertificate(certRA, generateCertificateResponse.getResult()
                .getPublicKeyIdentifier()).getResult();

        KeyStoreManager store = new KeyStoreManager();
        store.addAuthenticationKeyPair(authenticationKeyPair, passwd);
        store.addAuthenticationChain(passwd, result);
        store.store(storeLocation, passwd);

        Credential credential = new KeyStoreCredential(storeFilename, "authentication", password);
        KeyPair etkKeyPair = CertificateUtils.generateKeyPair();

        // start ETK registration
        Result<byte[]> challenge = etkRa.startETKRegistration(etkKeyPair.getPublic(), credential);

        // complete ETK registration
        X509Certificate etkCert = BuilderFactory.newEncryptionTokenBuilder(credential)
                .create()
                .withKeyPair(etkKeyPair)
                .withChallenge(challenge.getResult())
                .build();
        etkRa.completeETKRegistration(etkCert.getEncoded(), credential);

        store.addEncryptionToken(etkKeyPair, passwd, etkCert);
        store.store(storeLocation, passwd);

        // Certificate cannot be immediately renewed
        try {
            etkRa.activateToken(credential);
        } catch (RaException e) {
            Assert.assertEquals("[RENEW_ACTIVATION_ETK_ALREADY_ACTIF]", e.getErrorCodes()
                    .toString());
        }
    }

    @Test
    public void submitCSRForForeigner() throws TechnicalConnectorException {
        AuthenticationCertificateRegistrationService certRA = getAuthenticationCertificateRegistrationService();
        KeyPair authenticationKeyPair = CertificateUtils.generateKeyPair();
        SubmitCSRForForeignerResponseInfo submitCSRForForeignerResponseInfo = certRA.submitCSRForForeigner(BuilderFactory.newForeignRequestBuilder()
                        .withSsinBis("82451234769")
                        .withFirstName("foreignerFirstName")
                        .withName("foreignerName")
                        .withPersonalEmail("foreignerFirstName.foreignerName@mail.nl")
                        .withPersonalPhone("00444456464545")
                        .withKeyPair(authenticationKeyPair)
                        .build())
                .getResult();
        Assert.assertNotNull(submitCSRForForeignerResponseInfo.getValidationUrl());
    }

    private Result<X509Certificate[]> pollToGetCertificate(AuthenticationCertificateRegistrationService certRA, byte[] publicKeyIdentifier) throws TechnicalConnectorException, InterruptedException {
        Result<X509Certificate[]> result = certRA.getCertificate(publicKeyIdentifier);
        while (result.getStatus()
                .equals(Status.PENDING)) {
            long duration = new Duration(new DateTime(), result.getTime()).getMillis();
            if (duration > 0) {
                Thread.sleep(duration);
            }
            result = certRA.getCertificate(publicKeyIdentifier);
        }
        if (result.hasStatusError()) {
            throw new IllegalArgumentException(result.getStatus()
                    .name());
        }
        return result;
    }

    private String get(String key) {
        return rule.getSessionProperty(key);
    }

}
