/*
 * Copyright (c) Smals
 */
package be.fgov.ehealth.technicalconnector.tests.session;

import static be.ehealth.technicalconnector.utils.ConnectorXmlUtils.toDocument;
import static be.ehealth.technicalconnector.utils.TemplateEngineUtils.generate;
import static org.joda.time.DateTimeZone.UTC;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.InputStream;
import java.security.cert.CertificateEncodingException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import javax.security.auth.x500.X500Principal;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.joda.time.DateTime;
import org.junit.internal.AssumptionViolatedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;

import be.ehealth.technicalconnector.config.ConfigFactory;
import be.ehealth.technicalconnector.config.ConfigValidator;
import be.ehealth.technicalconnector.config.Configuration;
import be.ehealth.technicalconnector.exception.SessionManagementException;
import be.ehealth.technicalconnector.exception.TechnicalConnectorException;
import be.ehealth.technicalconnector.exception.TechnicalConnectorExceptionValues;
import be.ehealth.technicalconnector.idgenerator.IdGeneratorFactory;
import be.ehealth.technicalconnector.service.sts.SAMLTokenFactory;
import be.ehealth.technicalconnector.service.sts.security.SAMLToken;
import be.ehealth.technicalconnector.service.sts.security.impl.KeyStoreCredential;
import be.ehealth.technicalconnector.service.sts.utils.SAMLConverter;
import be.ehealth.technicalconnector.session.Session;
import be.ehealth.technicalconnector.session.SessionItem;
import be.ehealth.technicalconnector.session.SessionManager;
import be.ehealth.technicalconnector.session.impl.SessionManagerImpl;
import be.ehealth.technicalconnector.utils.ConnectorIOUtils;
import be.fgov.ehealth.technicalconnector.tests.utils.AssumeTools;
import be.fgov.ehealth.technicalconnector.tests.utils.LoggingUtils;
import be.fgov.ehealth.technicalconnector.tests.utils.SupportedLanguages;
import be.fgov.ehealth.technicalconnector.tests.utils.TestPropertiesLoader;


/**
 * utility methods to support integration tests. initializes config file and optionally the session. the config to use is determined by the
 * following properties
 * <ul>
 * <li>session.environment</li>
 * <li>session.professionType</li>
 * <li>session.username</li>
 * </ul>
 * <p>
 * the used property files from testcommons are
 * <ul>
 * <li>be.ehealth.technicalconnector-{env}-{professionType}-{user}.properties</li>
 * <li>be.ehealth.connector-session-{env}-{professionType}-{user}.properties</li>
 * </ul>
 * </p>
 *
 * @author Hannes De Clercq (ehd3)
 */
public final class SessionInitializer {


    private static final Logger LOG = LoggerFactory.getLogger(SessionInitializer.class);

    protected static final String DEFAULT_SESSION_CONFIG_FILE = "/be.ehealth.technicalconnector.test.properties";

    public static final String SESSION_USERNAME = "session.username";

    public static final String SESSION_PROFESSION_TYPE = "session.professionType";

    public static final String SESSION_ENVIRONMENT = "session.environment";

    private static final String TOKENNAME = "target/tokenAsString.token";

    private static final String TEST_SESSION_PERSIST = "test.session.persist";

    private static final String FILEPATH_PREFIX = "filepath.prefix";

    private static String defaultSessionType = SessionType.FALLBACK.name();

    private static Properties sessionProps;


    /**
     * Inner enum containing all the types of session supported by the connector;
     *
     * @author EHP
     */
    private enum SessionType {
        EID_ONLY, EID, FALLBACK
    }


    private SessionInitializer() {
        super();
    }

    public static void init() throws Exception {
        init(true);
    }

    public static void init(boolean init) throws Exception {
        init(DEFAULT_SESSION_CONFIG_FILE, init);
    }

    public static void init(String propsLocation) throws Exception {
        init(propsLocation, true);
    }


    public static void init(String propsLocation, boolean initSession) throws Exception {
        init(propsLocation, initSession, SupportedLanguages.JAVA.getAbbreviation());
    }

    public static void init(String propsLocation, String devLang) throws Exception {
        init(propsLocation, true, devLang);
    }

    public static void init(String propsLocation, boolean initSession, String devLang) throws Exception {
        Properties props = TestPropertiesLoader.getProperties(propsLocation);
        init(props, initSession, devLang);
    }

    public static void init(Properties props) throws Exception {
        init(props, true, SupportedLanguages.JAVA.getAbbreviation());
    }


    public static void init(Properties props, String devLang) throws Exception {
        init(props, true, devLang);
    }

    public static void init(Properties props, boolean initSession) throws Exception {
        init(props, initSession, SupportedLanguages.JAVA.getAbbreviation());
    }

    /**
     * This method will initialize the session based on the props as parameter it will use the following parameters as input <li>
     * session.environment <li>session.professionType <li>session.username
     */
    public static void init(Properties props, boolean initSession, String devLang) throws Exception {
        init(props, initSession, devLang, null);
    }

    /**
     * This method will initialize the session based on the props as parameter it will use the following parameters as input <li>
     * session.environment <li>session.professionType <li>session.username
     *
     * Extra test properties that will override any existing configuration property can also be supplied.
     */
    public static void init(Properties props, boolean initSession, String devLang, Properties overridingTestProps) throws Exception {
        LoggingUtils.bootstrap();
        Properties cleanedProps = TestPropertiesLoader.processProps(props, devLang);

        String prefix = "/";
        if (SupportedLanguages.JAVA.getAbbreviation()
                                   .equals(devLang)) {
            prefix = "/";
        } else if (SupportedLanguages.NET.getAbbreviation()
                                         .equals(devLang)) {
            java.lang.System.setProperty("https.protocols", "TLSv1,TLSv1.1,TLSv1.2");
            prefix = ".\\";
        }
        System.setProperty(FILEPATH_PREFIX, prefix); // So that we can access the prefix from everywhere, with a default value "/"
        initConfig(createLocation("be.ehealth.technicalconnector", cleanedProps, prefix, false, false), devLang);
        if (overridingTestProps != null) {
            for (Object keyObj : overridingTestProps.keySet()) {
                ConfigFactory.getConfigValidator()
                             .getConfig()
                             .setProperty(keyObj.toString(), overridingTestProps.getProperty(keyObj.toString()));
            }
        }
        loadSession(createLocation("be.ehealth.connector-session-test", cleanedProps, prefix, true, true), initSession, devLang);
    }


    private static void loadSession(String sessionPropsLocation, boolean initSession, String devLang) throws TechnicalConnectorException, SessionManagementException, Exception {
        sessionProps = TestPropertiesLoader.getProperties(sessionPropsLocation);

        String identPwd = sessionProps.getProperty("test.session.identification.password");
        String hokPwd = sessionProps.getProperty("test.session.holderofkey.password");
        String encPwd = sessionProps.getProperty("test.session.encryption.password");
        SessionType type = SessionType.valueOf(sessionProps.getProperty("test.sessiontype", defaultSessionType));

        Configuration config = ConfigFactory.getConfigValidator()
                                            .getConfig();

        for (Object keyObj : sessionProps.keySet()) {
            String key = keyObj.toString();
            if (!key.startsWith("test.")) {
                LOG.info("Adding key to ConfigFactory:" + key);
                String value = sessionProps.getProperty(key);
                if (value.startsWith("/") && SupportedLanguages.NET.getAbbreviation()
                                                                   .equals(devLang)) {
                    value = StringUtils.replaceOnce(value, "/", ".\\\\");
                    LOG.info("Replacing [" + key + "] with value [" + value + "]");
                }
                config.setProperty(key, value);
            }
        }


        LOG.info("Reloading configuration modules");
        config.reload();

        LOG.info("Starting new session");
        if (initSession) {
            SessionManager sessionmgmt = Session.getInstance();
            if (!sessionmgmt.hasValidSession()) {
                switch (type) {
                    case EID:
                        LOG.info("Creating session of type " + SessionType.EID);
                        sessionmgmt.createSession(hokPwd, encPwd);
                        break;
                    case EID_ONLY:
                        LOG.info("Creating session of type " + SessionType.EID_ONLY);
                        sessionmgmt.createSessionEidOnly();
                        break;
                    case FALLBACK:
                        LOG.info("Creating session of type " + SessionType.FALLBACK);
                        sessionmgmt.createFallbackSession(identPwd, hokPwd, encPwd);
                        break;
                    default:
                        break;
                }
            }
            String persistToken = sessionProps.getProperty(TEST_SESSION_PERSIST, "false");
            if ("true".equalsIgnoreCase(persistToken)) {
                LOG.info("Persisting session to file");
                storeAndReload(identPwd, hokPwd, encPwd, sessionmgmt);
            }
        } else {
            LOG.info("Generating Assertion");
            config.setProperty(SessionManagerImpl.PROP_FETCH_ETK, "false");
            Session.getInstance()
                   .loadSession(generateSamlToken(identPwd, hokPwd, config), hokPwd, encPwd);

            try {
                AssumeTools.isInternetConnectionEnabled();
            } catch (AssumptionViolatedException e) {
                offlineModus();
            }

        }
    }


    private static SAMLToken generateSamlToken(String identPwd, String hokPwd, Configuration config) throws TechnicalConnectorException, CertificateEncodingException {


        String identKeyStore = config.getProperty("sessionmanager.identification.keystore");
        String identAlias = config.getProperty("sessionmanager.identification.alias", "authentication");
        KeyStoreCredential identificationCredential = new KeyStoreCredential(identKeyStore, identAlias, identPwd);

        String hokKeystore = config.getProperty("sessionmanager.holderofkey.keystore");
        String hokAlias = config.getProperty("sessionmanager.holderofkey.alias", "authentication");
        KeyStoreCredential hokCredential = new KeyStoreCredential(hokKeystore, hokAlias, hokPwd);

        Map<String, Object> ctx = new HashMap<String, Object>();
        ctx.put("uuid", IdGeneratorFactory.getIdGenerator(IdGeneratorFactory.UUID)
                                          .generateId());
        ctx.put("now", new DateTime().toDateTime(UTC));
        ctx.put("NotBefore", new DateTime().toDateTime(UTC));
        ctx.put("NotOnOrAfter", new DateTime().plusHours(1)
                                              .toDateTime(UTC));
        ctx.put("identification-ca", StringEscapeUtils.escapeXml(identificationCredential.getCertificate()
                                                                                         .getIssuerX500Principal()
                                                                                         .getName(X500Principal.RFC1779)));
        ctx.put("identification-cn", StringEscapeUtils.escapeXml(identificationCredential.getCertificate()
                                                                                         .getSubjectX500Principal()
                                                                                         .getName(X500Principal.RFC1779)));
        ctx.put("holder-of-key", new String(Base64.encodeBase64Chunked(hokCredential.getCertificate()
                                                                                    .getEncoded())));
        ctx.put("attrList", config.getMatchingProperties("sessionmanager.samlattribute"));
        ctx.put("attrdesignatorList", config.getMatchingProperties("sessionmanager.samlattributedesignator"));
        ctx.put("ssin", config.getProperty("user.inss"));
        ctx.put("nihii11", config.getProperty("user.nihii"));

        String assertion = generate(ctx, "/templates/saml1_1.assertion");
        Element savedAssertion = toDocument(assertion).getDocumentElement();
        return SAMLTokenFactory.getInstance()
                               .createSamlToken(savedAssertion, hokCredential);
    }

    private static void offlineModus() throws TechnicalConnectorException {
        LOG.info("Offline modus detected!.");
        ConfigFactory.getConfigValidator()
                     .setProperty("crypto.revocationstatuschecker.classname", "be.ehealth.technicalconnector.service.etee.impl.ConnectorMockRevocationStatusChecker");
    }


    private static void storeAndReload(String indentPwd, String hokPwd, String encPwd, SessionManager sessionmgmt) throws Exception {
        SessionItem item = sessionmgmt.getSession();

        Element originalAssertion = item.getSAMLToken()
                                        .getAssertion();
        String serializedToken = SAMLConverter.toXMLString(originalAssertion);

        File temp = new File(TOKENNAME);
        temp.deleteOnExit();

        IOUtils.write(serializedToken.getBytes(), new FileOutputStream(temp));

        sessionmgmt.unloadSession();

        Element savedAssertion = SAMLConverter.toElement(IOUtils.toString(new FileReader(temp)));

        ConfigValidator config = ConfigFactory.getConfigValidator();

        String hokKeystore = config.getProperty("sessionmanager.holderofkey.keystore");
        String hokAlias = config.getProperty("sessionmanager.holderofkey.alias", "authentication");

        SAMLToken token = SAMLTokenFactory.getInstance()
                                          .createSamlToken(savedAssertion, new KeyStoreCredential(hokKeystore, hokAlias, hokPwd));

        sessionmgmt.loadSession(token, hokPwd, encPwd);
    }


    private static void initConfig(String location, String devLang) throws TechnicalConnectorException {
        try {
            InputStream in = ConnectorIOUtils.getResourceAsStream(location, false);
            final File tempFile = File.createTempFile("SessionInitializer", ".properties");
            tempFile.deleteOnExit();
            Properties configProps = new Properties();
            configProps.load(in);
            if (SupportedLanguages.NET.getAbbreviation()
                                      .equals(devLang)) {
                Enumeration<?> e = configProps.propertyNames();
                while (e.hasMoreElements()) {
                    String key = (String) e.nextElement();
                    String value = configProps.getProperty(key);
                    if (value.startsWith("/")) {
                        value = StringUtils.replaceOnce(value, "/", ".\\\\");
                        LOG.info("Replacing [" + key + "] with value [" + value + "]");
                        configProps.setProperty(key, value);
                    }
                }
            }
            configProps.store(new FileOutputStream(tempFile), "Process properties for " + devLang);
            ConfigFactory.setConfigLocation(tempFile.getAbsolutePath());
        } catch (Exception e) {
            throw new TechnicalConnectorException(TechnicalConnectorExceptionValues.ERROR_GENERAL, e.getMessage(), e);
        }
    }

    private static String createLocation(String baseName, Properties props, String prefix, boolean includeEnv, boolean includeName) throws Exception {
        String env = null;
        String profession = null;
        String name = null;
        for (Object keyObj : props.keySet()) {
            String key = keyObj.toString();
            if (key.equalsIgnoreCase(SESSION_ENVIRONMENT)) {
                env = props.getProperty(key);
            } else if (key.equalsIgnoreCase(SESSION_PROFESSION_TYPE)) {
                profession = props.getProperty(key);
            } else if (key.equalsIgnoreCase(SESSION_USERNAME)) {
                name = props.getProperty(key);
            }
        }

        StringBuilder sb = new StringBuilder();
        if (includeEnv && !StringUtils.isEmpty(env)) {
            sb.append("-")
              .append(env);
        }
        if (!StringUtils.isEmpty(profession)) {
            sb.append("-")
              .append(profession);
        }
        if (includeName && !StringUtils.isEmpty(name)) {
            sb.append("-")
              .append(name);
        }
        if (StringUtils.isEmpty(sb.toString()) && "be.ehealth.connector-session-test".equals(baseName)) {
            baseName = "be.ehealth.technicalconnector.test";
        }
        return prefix + baseName + sb.toString() + ".properties";
    }


    /**
     * @return the sessionProps
     */
    public static Properties getSessionProps() {
        return sessionProps;
    }

    public static String getSessionProperty(String key) {
        return sessionProps.getProperty(key);
    }

}
