package org.apereo.cas.util;

import com.unboundid.ldap.sdk.AddRequest;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.LDAPConnection;
import org.ldaptive.AttributeModification;
import org.ldaptive.AttributeModificationType;
import org.ldaptive.Connection;
import org.ldaptive.DefaultConnectionFactory;
import org.ldaptive.LdapAttribute;
import org.ldaptive.LdapEntry;
import org.ldaptive.ModifyOperation;
import org.ldaptive.ModifyRequest;
import org.ldaptive.io.LdifReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.stream.Collectors;

/**
 * Utility class used by all tests that provision and deprovision LDAP test data.
 *
 * @author Marvin S. Addison
 * @since 4.0.0
 */
public final class LdapTestUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(LdapTestUtils.class);

    /**
     * Placeholder for base DN in LDIF files.
     */
    private static final String BASE_DN_PLACEHOLDER = "${ldapBaseDn}";

    /**
     * System-wide newline character string.
     */
    private static final String NEWLINE = System.getProperty("line.separator");

    /**
     * Private constructor of utility class.
     */
    private LdapTestUtils() {
    }

    /**
     * Reads an LDIF into a collection of LDAP entries. The components performs a simple property
     * replacement in the LDIF data where <pre>${ldapBaseDn}</pre> is replaced with the environment-specific base
     * DN.
     *
     * @param ldif   LDIF resource, typically a file on filesystem or classpath.
     * @param baseDn The directory branch where the entry resides.
     * @return LDAP entries contained in the LDIF.
     * @throws IOException On IO errors reading LDIF.
     */
    public static Collection<LdapEntry> readLdif(final InputStream ldif, final String baseDn) throws IOException {
        final String ldapString;
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(ldif))) {
            ldapString = reader.lines()
                    .map(line -> {
                        if (line.contains(BASE_DN_PLACEHOLDER)) {
                            return line.replace(BASE_DN_PLACEHOLDER, baseDn);
                        }
                        return line;
                    })
                    .collect(Collectors.joining(NEWLINE));
        }
        return new LdifReader(new StringReader(ldapString)).read().getEntries();
    }

    /**
     * Creates the given LDAP entries.
     *
     * @param connection Open LDAP connection used to connect to directory.
     * @param entries    Collection of LDAP entries.
     * @throws Exception On LDAP errors.
     */
    public static void createLdapEntries(final LDAPConnection connection, final Collection<LdapEntry> entries) throws Exception {
        try {
            for (final LdapEntry entry : entries) {
                final Collection<Attribute> attrs = new ArrayList<>(entry.getAttributeNames().length);
                attrs.addAll(entry.getAttributes().stream()
                        .map(a -> new Attribute(a.getName(), a.getStringValues())).collect(Collectors.toList()));

                final AddRequest ad = new AddRequest(entry.getDn(), attrs);
                connection.add(ad);
            }
        } catch (final Exception e) {
            LOGGER.warn(e.getLocalizedMessage());
        }
    }

    /**
     * Modify ldap entry.
     *
     * @param serverCon the server con
     * @param dn        the dn
     * @param attr      the attr
     * @param add       the add
     */
    public static void modifyLdapEntry(final LDAPConnection serverCon, final String dn, final LdapAttribute attr,
                                       final AttributeModificationType add) {
        try {
            final String address = "ldap://" + serverCon.getConnectedAddress() + ':' + serverCon.getConnectedPort();
            try (Connection conn = DefaultConnectionFactory.getConnection(address)) {
                try {
                    conn.open();
                    final ModifyOperation modify = new ModifyOperation(conn);
                    modify.execute(new ModifyRequest(dn, new AttributeModification(add, attr)));
                } catch (final Exception e) {
                    LOGGER.debug(e.getMessage(), e);
                }
            }
        } finally {
            serverCon.close();
        }
    }

    /**
     * Modify ldap entry.
     *
     * @param serverCon the server con
     * @param dn        the dn
     * @param attr      the attr
     */
    public static void modifyLdapEntry(final LDAPConnection serverCon, final LdapEntry dn, final LdapAttribute attr) {
        modifyLdapEntry(serverCon, dn.getDn(), attr, AttributeModificationType.ADD);
    }
}
