/*
 *  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
 * 
 *    https://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.ldap.client.api;


import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.apache.directory.api.asn1.DecoderException;
import org.apache.directory.api.asn1.util.Oid;
import org.apache.directory.api.ldap.extras.extended.startTls.StartTlsRequest;
import org.apache.directory.api.ldap.extras.extended.startTls.StartTlsRequestImpl;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.message.BindRequest;
import org.apache.directory.api.ldap.model.message.BindRequestImpl;
import org.apache.directory.api.ldap.model.message.BindResponse;
import org.apache.directory.api.ldap.model.message.ExtendedRequest;
import org.apache.directory.api.ldap.model.message.ExtendedResponse;
import org.apache.directory.api.ldap.model.name.Dn;
import org.junit.jupiter.api.Test;
import org.mockito.exceptions.misusing.NotAMockException;
import org.mockito.internal.util.MockUtil;
import org.mockito.verification.VerificationMode;


public class ValidatingPoolableLdapConnectionFactoryTest
{
    private static final String ADMIN_CREDENTIALS = "secret";
    private static final String ADMIN_DN = "uid=admin, ou=system";

    @Test
    public void testPoolWithBind()
    {
        PoolTester tester = new PoolTester();

        // no bind
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // bind()
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    connection.bind();
                    verifyConnection( connection, times( 1 ) ).bind();
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // anonymousBind()
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    connection.anonymousBind();
                    verifyConnection( connection, times( 1 ) ).anonymousBind();
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // bind( String )
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    connection.bind( "" );
                    verifyConnection( connection, times( 1 ) ).bind( "" );
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // admin bind( String, String )
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    connection.bind( ADMIN_DN, ADMIN_CREDENTIALS );
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // bind( String, String )
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    connection.bind( "", "" );
                    verifyConnection( connection, times( 1 ) ).bind( "", "" );
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // bind( Dn )
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    Dn dn = new Dn();
                    connection.bind( dn );
                    verifyConnection( connection, times( 1 ) ).bind( dn );
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // bind( Dn, String )
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    Dn dn = new Dn();
                    connection.bind( dn, "" );
                    verifyConnection( connection, times( 1 ) ).bind( dn, "" );
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // bind( BindRequest );
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    BindRequest bindRequest = new BindRequestImpl();
                    connection.bind( bindRequest );
                    verifyConnection( connection, times( 1 ) ).bind( bindRequest );
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );
    }


    @Test
    public void testPoolWithStartTls()
    {
        PoolTester tester = new PoolTester();

        // extended( String )
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    connection.extended( StartTlsRequest.EXTENSION_OID );
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // extended( String, byte[] )
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    connection.extended( StartTlsRequest.EXTENSION_OID, new byte[]
                        {} );
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // extended( Oid )
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    try
                    {
                        connection.extended( Oid.fromString( StartTlsRequest.EXTENSION_OID ) );
                    }
                    catch ( DecoderException e )
                    {
                        throw new IllegalArgumentException( "invalid oid: " + StartTlsRequest.EXTENSION_OID );
                    }
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // extended( Oid, byte[] )
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    try
                    {
                        connection.extended( Oid.fromString( StartTlsRequest.EXTENSION_OID ), new byte[]
                            {} );
                    }
                    catch ( DecoderException e )
                    {
                        throw new IllegalArgumentException( "invalid oid: " + StartTlsRequest.EXTENSION_OID );
                    }
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );

        // extended( ExtendedRequest )
        tester.execute(
            new WithConnection()
            {
                @Override
                public void execute( LdapConnection connection, Counts counts ) throws LdapException
                {
                    connection.extended( new StartTlsRequestImpl() );
                    verifyAdminBind( connection, times( counts.adminBindCount ) );
                }
            } );
    }


    private static final void verifyAdminBind( LdapConnection connection, VerificationMode mode ) throws LdapException
    {
        verifyConnection( connection, mode ).bind( ADMIN_DN, ADMIN_CREDENTIALS );
    }


    private static final LdapConnection verifyConnection( LdapConnection connection, VerificationMode mode )
    {
        if ( MockUtil.isMock( connection ) )
        {
            return org.mockito.Mockito.verify( connection, mode );
        }
        else
        {
            if ( connection instanceof Wrapper )
            {
                @SuppressWarnings("unchecked")
                LdapConnection unwrapped = ( ( Wrapper<LdapConnection> ) connection ).wrapped();
                
                return verifyConnection( unwrapped, mode );
            }
        }
        throw new NotAMockException( "connection is not a mock, nor a wrapper for a connection that is one" );
    }

    private static final class Counts
    {
        private int adminBindCount = 0;
        private int unBindCount = 0;
    }

    public static final class InternalMonitoringLdapConnection extends LdapConnectionWrapper
    {
        private static final Oid START_TLS_OID;

        static
        {
            try
            {
                START_TLS_OID = Oid.fromString( StartTlsRequest.EXTENSION_OID );
            }
            catch ( DecoderException e )
            {
                throw new IllegalStateException( "StartTlsRequest.EXTENSION_OID is not a valid oid...  IMPOSSIBLE" );
            }
        }

        private int borrowedCount = 0;
        private boolean bindCalled = false;
        private Counts counts = new Counts();
        private boolean startTlsCalled = false;
        private boolean unBindCalled = false;


        InternalMonitoringLdapConnection( LdapConnection connection )
        {
            super( connection );
        }


        private int incrementBorrowedCount()
        {
            return ++borrowedCount;
        }


        public boolean bindCalled()
        {
            return bindCalled;
        }


        public void resetMonitors()
        {
            bindCalled = false;
            startTlsCalled = false;
            unBindCalled = false;
        }


        public boolean startTlsCalled()
        {
            return startTlsCalled;
        }


        public boolean unBindCalled()
        {
            return unBindCalled;
        }


        @Override
        public void bind() throws LdapException
        {
            connection.bind();
            bindCalled = true;
        }


        @Override
        public void anonymousBind() throws LdapException
        {
            connection.anonymousBind();
            bindCalled = true;
        }


        @Override
        public void bind( String name ) throws LdapException
        {
            connection.bind( name );
            bindCalled = true;
        }


        @Override
        public void bind( String name, String credentials ) throws LdapException
        {
            connection.bind( name, credentials );
            if ( ADMIN_DN.equals( name )
                && ADMIN_CREDENTIALS.equals( credentials ) )
            {
                counts.adminBindCount++;
            }
            bindCalled = true;
        }


        @Override
        public void bind( Dn name ) throws LdapException
        {
            connection.bind( name );
            bindCalled = true;
        }


        @Override
        public void bind( Dn name, String credentials ) throws LdapException
        {
            connection.bind( name, credentials );
            bindCalled = true;
        }


        @Override
        public BindResponse bind( BindRequest bindRequest ) throws LdapException
        {
            BindResponse response = connection.bind( bindRequest );
            bindCalled = true;
            return response;
        }


        @Override
        public ExtendedResponse extended( String oid ) throws LdapException
        {
            if ( StartTlsRequest.EXTENSION_OID.equals( oid ) )
            {
                startTlsCalled = true;
            }
            return connection.extended( oid );
        }


        @Override
        public ExtendedResponse extended( String oid, byte[] value ) throws LdapException
        {
            if ( StartTlsRequest.EXTENSION_OID.equals( oid ) )
            {
                startTlsCalled = true;
            }
            return connection.extended( oid, value );
        }


        @Override
        public ExtendedResponse extended( Oid oid ) throws LdapException
        {
            if ( START_TLS_OID.equals( oid ) )
            {
                startTlsCalled = true;
            }
            return connection.extended( oid );
        }


        @Override
        public ExtendedResponse extended( Oid oid, byte[] value ) throws LdapException
        {
            if ( START_TLS_OID.equals( oid ) )
            {
                startTlsCalled = true;
            }
            return connection.extended( oid, value );
        }


        @Override
        public ExtendedResponse extended( ExtendedRequest extendedRequest ) throws LdapException
        {
            if ( extendedRequest.hasControl( StartTlsRequest.EXTENSION_OID ) )
            {
                startTlsCalled = true;
            }
            return connection.extended( extendedRequest );
        }


        @Override
        public void unBind() throws LdapException
        {
            counts.unBindCount++;
            unBindCalled = true;
            connection.unBind();
        }
    }

    private static class PoolTester
    {
        private LdapConnectionConfig config;
        private LdapConnectionPool pool;
        private LdapConnectionValidator validator;


        public PoolTester()
        {
            LdapConnection mockConnection = mock( LdapConnection.class );
            when( mockConnection.isAuthenticated() ).thenReturn( true );
            when( mockConnection.isConnected() ).thenReturn( true );

            config = new LdapConnectionConfig();
            config.setName( ADMIN_DN );
            config.setCredentials( ADMIN_CREDENTIALS );

            MockLdapConnectionFactory mockConnectionFactory = new MockLdapConnectionFactory( config );
            mockConnectionFactory.addConnection(
                new InternalMonitoringLdapConnection( mockConnection ) );

            validator = mock( LdapConnectionValidator.class );
            when( validator.validate( any( LdapConnection.class ) ) ).thenReturn( true );
            ValidatingPoolableLdapConnectionFactory poolableFactory =
                new ValidatingPoolableLdapConnectionFactory( mockConnectionFactory );
            poolableFactory.setValidator( validator );

            pool = new LdapConnectionPool( poolableFactory );
            pool.setMaxTotal( 1 );
            pool.setTestOnBorrow( true );
            pool.setTestOnReturn( true );
        }


        public void execute( WithConnection withConnection )
        {
            LdapConnection connection = null;
            InternalMonitoringLdapConnection internal = null;
            int borrowedCount = 0;
            try
            {
                connection = pool.getConnection();
                assertNotNull( connection );
                internal = ( InternalMonitoringLdapConnection ) unwrap( unwrap( connection ) ); // two levels of wrapping
                borrowedCount = internal.incrementBorrowedCount();
                verify( validator, times( 2 * borrowedCount - 1 ) ).validate( unwrap( connection ) );
                internal.resetMonitors();

                withConnection.execute( connection, internal.counts );
            }
            catch ( LdapException e )
            {
                fail( "unable to getConnection(): " + e.getMessage() );
            }
            finally
            {
                try
                {
                    int adminBindCount = internal.counts.adminBindCount;
                    pool.releaseConnection( connection );
                    verify( validator, times( 2 * borrowedCount ) ).validate( unwrap(connection) );

                    if ( internal.startTlsCalled() )
                    {
                        verifyConnection( connection, times( internal.counts.unBindCount ) ).unBind();
                    }

                    int expectedCount = internal.bindCalled() || internal.startTlsCalled() || internal.unBindCalled()
                        ? adminBindCount + 1
                        : adminBindCount;
                    verifyAdminBind( connection, times( expectedCount ) );
                }
                catch ( LdapException e )
                {
                    fail( "unable to releaseConnection(): " + e.getMessage() );
                }
            }
        }
    }

    
    private static LdapConnection unwrap( LdapConnection connection )
    {
        return ( ( LdapConnectionWrapper ) connection ).wrapped();
    }
    
    
    private static interface WithConnection
    {
        /** 
         * Executes code using the supplied connection.
         * 
         * @param connection The ldap connection
         * @param counts The counters for specific calls
         */
        public void execute( LdapConnection connection, Counts counts ) throws LdapException;
    }
}
