/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.directory.server.core.integ;


import java.lang.reflect.Method;
import java.util.UUID;

import org.apache.directory.api.ldap.model.entry.DefaultModification;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.entry.ModificationOperation;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.util.FileUtils;
import org.apache.directory.server.annotations.CreateKdcServer;
import org.apache.directory.server.annotations.CreateLdapServer;
import org.apache.directory.server.constants.ServerDNConstants;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.changelog.ChangeLog;
import org.apache.directory.server.core.factory.DSAnnotationProcessor;
import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory;
import org.apache.directory.server.core.factory.DirectoryServiceFactory;
import org.apache.directory.server.core.factory.PartitionFactory;
import org.apache.directory.server.core.security.TlsKeyGenerator;
import org.apache.directory.server.factory.ServerAnnotationProcessor;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.server.kerberos.kdc.KdcServer;
import org.apache.directory.server.ldap.LdapServer;
import org.junit.Ignore;
import org.junit.runner.Description;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * The class responsible for running all the tests. t read the annotations, 
 * initialize the DirectoryService, call each test and do the cleanup at the end.
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class FrameworkRunner extends BlockJUnit4ClassRunner
{
    /** A logger for this class */
    private static final Logger LOG = LoggerFactory.getLogger( FrameworkRunner.class );

    /** The 'service' field in the run tests */
    private static final String SET_SERVICE_METHOD_NAME = "setService";

    /** The 'ldapServer' field in the run tests */
    private static final String SET_LDAP_SERVER_METHOD_NAME = "setLdapServer";

    /** The 'kdcServer' field in the run tests */
    private static final String SET_KDC_SERVER_METHOD_NAME = "setKdcServer";

    /** The DirectoryService for this class, if any */
    private DirectoryService classDS;

    /** The LdapServer for this class, if any */
    private LdapServer classLdapServer;

    /** The KdcServer for this class, if any */
    private KdcServer classKdcServer;


    /**
     * Creates a new instance of FrameworkRunner.
     */
    public FrameworkRunner( Class<?> clazz ) throws InitializationError
    {
        super( clazz );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void run( final RunNotifier notifier )
    {
        // Before running any test, check to see if we must create a class DS
        // Get the LdapServerBuilder, if any
        CreateLdapServer classLdapServerBuilder = getDescription().getAnnotation( CreateLdapServer.class );

        try
        {
            classDS = DSAnnotationProcessor.getDirectoryService( getDescription() );
            long revision = 0L;
            DirectoryService directoryService = null;

            if ( classDS != null )
            {
                // We have a class DS defined, update it
                directoryService = classDS;

                DSAnnotationProcessor.applyLdifs( getDescription(), classDS );
            }
            else
            {
                // No : define a default class DS then
                DirectoryServiceFactory dsf = DefaultDirectoryServiceFactory.class.newInstance();

                directoryService = dsf.getDirectoryService();
                // enable CL explicitly cause we are not using DSAnnotationProcessor
                directoryService.getChangeLog().setEnabled( true );

                dsf.init( "default" + UUID.randomUUID().toString() );

                // Stores the defaultDS in the classDS
                classDS = directoryService;

                // Load the schemas
                DSAnnotationProcessor.loadSchemas( getDescription(), directoryService );

                // Apply the class LDIFs
                DSAnnotationProcessor.applyLdifs( getDescription(), directoryService );
            }

            updateTlsKey( classDS );

            // check if it has a LdapServerBuilder
            // then use the DS created above
            if ( classLdapServerBuilder != null )
            {
                classLdapServer = ServerAnnotationProcessor.createLdapServer( getDescription(), directoryService );
            }

            if ( classKdcServer == null )
            {
                int minPort = getMinPort();

                classKdcServer = ServerAnnotationProcessor.getKdcServer( getDescription(), directoryService,
                    minPort + 1 );
            }

            // print out information which partition factory we use
            DirectoryServiceFactory dsFactory = DefaultDirectoryServiceFactory.class.newInstance();
            PartitionFactory partitionFactory = dsFactory.getPartitionFactory();
            LOG.debug( "Using partition factory {}", partitionFactory.getClass().getSimpleName() );

            // Now run the class
            super.run( notifier );

            if ( classLdapServer != null )
            {
                classLdapServer.stop();
            }

            if ( classKdcServer != null )
            {
                classKdcServer.stop();
            }

            // cleanup classService if it is not the same as suite service or
            // it is not null (this second case happens in the absence of a suite)
            if ( classDS != null )
            {
                LOG.debug( "Shuting down DS for {}", classDS.getInstanceId() );
                classDS.shutdown();
                FileUtils.deleteDirectory( classDS.getInstanceLayout().getInstanceDirectory() );
            }
            else
            {
                // Revert the ldifs
                // We use a class or suite DS, just revert the current test's modifications
                revert( directoryService, revision );
            }
        }
        catch ( Exception e )
        {
            LOG.error( I18n.err( I18n.ERR_181, getTestClass().getName() ) );
            LOG.error( e.getLocalizedMessage() );
            notifier.fireTestFailure( new Failure( getDescription(), e ) );
        }
        finally
        {
            // help GC to get rid of the directory service with all its references
            classDS = null;
            classLdapServer = null;
            classKdcServer = null;
        }
    }


    /**
     * Get the lower port out of all the transports
     */
    private int getMinPort()
    {
        int minPort = 0;

        return minPort;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    protected void runChild( FrameworkMethod method, RunNotifier notifier )
    {
        /** The LdapServer for this method, if any */
        LdapServer methodLdapServer = null;

        /** The KdcServer for this method, if any */
        KdcServer methodKdcServer = null;

        // Don't run the test if the @Ignored annotation is used
        if ( method.getAnnotation( Ignore.class ) != null )
        {
            Description description = describeChild( method );
            notifier.fireTestIgnored( description );
            return;
        }

        // Get the applyLdifs for each level
        Description suiteDescription = null;

        Description classDescription = getDescription();
        Description methodDescription = describeChild( method );

        // Before running any test, check to see if we must create a class DS
        // Get the LdapServerBuilder, if any
        CreateLdapServer methodLdapServerBuilder = methodDescription.getAnnotation( CreateLdapServer.class );
        CreateKdcServer methodKdcServerBuilder = methodDescription.getAnnotation( CreateKdcServer.class );

        // Ok, ready to run the test
        try
        {
            DirectoryService directoryService = null;

            // Set the revision to 0, we will revert only if it's set to another value
            long revision = 0L;

            // Check if this method has a dedicated DSBuilder
            DirectoryService methodDS = DSAnnotationProcessor.getDirectoryService( methodDescription );

            // give #1 priority to method level DS if present
            if ( methodDS != null )
            {
                // Apply all the LDIFs
                DSAnnotationProcessor.applyLdifs( suiteDescription, methodDS );
                DSAnnotationProcessor.applyLdifs( classDescription, methodDS );
                DSAnnotationProcessor.applyLdifs( methodDescription, methodDS );

                directoryService = methodDS;

                updateTlsKey( directoryService );
            }
            else if ( classDS != null )
            {
                directoryService = classDS;

                // apply the method LDIFs, and tag for reversion
                revision = getCurrentRevision( directoryService );

                DSAnnotationProcessor.applyLdifs( methodDescription, directoryService );
            }
            // we don't support method level LdapServer so
            // we check for the presence of Class level LdapServer first 
            else if ( classLdapServer != null )
            {
                directoryService = classLdapServer.getDirectoryService();

                revision = getCurrentRevision( directoryService );

                DSAnnotationProcessor.applyLdifs( methodDescription, directoryService );
            }
            else if ( classKdcServer != null )
            {
                directoryService = classKdcServer.getDirectoryService();

                revision = getCurrentRevision( directoryService );

                DSAnnotationProcessor.applyLdifs( methodDescription, directoryService );
            }

            if ( methodLdapServerBuilder != null )
            {
                methodLdapServer = ServerAnnotationProcessor.createLdapServer( methodDescription, directoryService );
            }

            if ( methodKdcServerBuilder != null )
            {
                int minPort = getMinPort();

                methodKdcServer = ServerAnnotationProcessor.getKdcServer( methodDescription, directoryService,
                    minPort + 1 );
            }

            // At this point, we know which service to use.
            // Inject it into the class
            Method setService = null;

            try
            {
                setService = getTestClass().getJavaClass().getMethod( SET_SERVICE_METHOD_NAME,
                    DirectoryService.class );

                setService.invoke( getTestClass().getJavaClass(), directoryService );
            }
            catch ( NoSuchMethodException nsme )
            {
                // Do nothing
            }

            // if we run this class in a suite, tell it to the test
            Method setLdapServer = null;

            try
            {
                setLdapServer = getTestClass().getJavaClass().getMethod( SET_LDAP_SERVER_METHOD_NAME,
                    LdapServer.class );
            }
            catch ( NoSuchMethodException nsme )
            {
                // Do nothing
            }

            Method setKdcServer = null;

            try
            {
                setKdcServer = getTestClass().getJavaClass().getMethod( SET_KDC_SERVER_METHOD_NAME, KdcServer.class );
            }
            catch ( NoSuchMethodException nsme )
            {
                // Do nothing
            }

            DirectoryService oldLdapServerDirService = null;
            DirectoryService oldKdcServerDirService = null;

            if ( methodLdapServer != null )
            {
                // setting the directoryService is required to inject the correct level DS instance in the class or suite level LdapServer
                methodLdapServer.setDirectoryService( directoryService );

                setLdapServer.invoke( getTestClass().getJavaClass(), methodLdapServer );
            }
            else if ( classLdapServer != null )
            {
                oldLdapServerDirService = classLdapServer.getDirectoryService();

                // setting the directoryService is required to inject the correct level DS instance in the class or suite level LdapServer
                classLdapServer.setDirectoryService( directoryService );

                setLdapServer.invoke( getTestClass().getJavaClass(), classLdapServer );
            }

            if ( methodKdcServer != null )
            {
                // setting the directoryService is required to inject the correct level DS instance in the class or suite level KdcServer
                methodKdcServer.setDirectoryService( directoryService );

                setKdcServer.invoke( getTestClass().getJavaClass(), methodKdcServer );
            }
            else if ( classKdcServer != null )
            {
                oldKdcServerDirService = classKdcServer.getDirectoryService();

                // setting the directoryService is required to inject the correct level DS instance in the class or suite level KdcServer
                classKdcServer.setDirectoryService( directoryService );

                setKdcServer.invoke( getTestClass().getJavaClass(), classKdcServer );
            }

            // Run the test
            super.runChild( method, notifier );

            if ( methodLdapServer != null )
            {
                methodLdapServer.stop();
            }

            if ( oldLdapServerDirService != null )
            {
                classLdapServer.setDirectoryService( oldLdapServerDirService );
            }

            if ( oldKdcServerDirService != null )
            {
                classKdcServer.setDirectoryService( oldKdcServerDirService );
            }

            // Cleanup the methodDS if it has been created
            if ( methodDS != null )
            {
                LOG.debug( "Shuting down DS for {}", methodDS.getInstanceId() );
                methodDS.shutdown();
                FileUtils.deleteDirectory( methodDS.getInstanceLayout().getInstanceDirectory() );
            }
            else
            {
                // We use a class or suite DS, just revert the current test's modifications
                revert( directoryService, revision );
            }
        }
        catch ( Exception e )
        {
            LOG.error( I18n.err( I18n.ERR_182, method.getName() ) );
            LOG.error( "", e );
            notifier.fireTestFailure( new Failure( getDescription(), e ) );
        }
    }


    private long getCurrentRevision( DirectoryService dirService ) throws Exception
    {
        if ( ( dirService != null ) && ( dirService.getChangeLog().isEnabled() ) )
        {
            long revision = dirService.getChangeLog().getCurrentRevision();
            LOG.debug( "Create revision {}", revision );

            return revision;
        }

        return 0;
    }


    private void revert( DirectoryService dirService, long revision ) throws Exception
    {
        if ( dirService == null )
        {
            return;
        }

        ChangeLog cl = dirService.getChangeLog();
        if ( cl.isEnabled() && ( revision < cl.getCurrentRevision() ) )
        {
            LOG.debug( "Revert revision {}", revision );
            dirService.revert( revision );
        }
    }


    private void updateTlsKey( DirectoryService ds ) throws LdapException, LdapInvalidDnException
    {
        // Update TLS key for tests. Newer Java 8 releases consider RSA keys
        // with less than 1024 bits as insecure and such are disabled by default, see 
        // http://www.oracle.com/technetwork/java/javase/8-compatibility-guide-2156366.html
        Entry adminEntry = ds.getAdminSession().lookup( new Dn( ServerDNConstants.ADMIN_SYSTEM_DN ) );
        TlsKeyGenerator.addKeyPair( adminEntry, TlsKeyGenerator.CERTIFICATE_PRINCIPAL_DN,
            TlsKeyGenerator.CERTIFICATE_PRINCIPAL_DN, "RSA", 1024 );
        Modification mod1 = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
            adminEntry.get( TlsKeyGenerator.PRIVATE_KEY_AT ) );
        Modification mod2 = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
            adminEntry.get( TlsKeyGenerator.PUBLIC_KEY_AT ) );
        Modification mod3 = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
            adminEntry.get( TlsKeyGenerator.USER_CERTIFICATE_AT ) );
        ds.getAdminSession().modify( adminEntry.getDn(), mod1, mod2, mod3 );
    }
}
