/*
 *   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.shared.kerberos.codec;


import static org.junit.Assert.assertTrue;

import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.directory.api.asn1.DecoderException;
import org.apache.directory.api.asn1.ber.Asn1Decoder;
import org.apache.directory.api.util.Network;
import org.apache.directory.api.util.Strings;
import org.apache.directory.shared.kerberos.KerberosTime;
import org.apache.directory.shared.kerberos.codec.encKrbCredPart.EncKrbCredPartContainer;
import org.apache.directory.shared.kerberos.codec.types.EncryptionType;
import org.apache.directory.shared.kerberos.components.EncKrbCredPart;
import org.apache.directory.shared.kerberos.components.EncryptionKey;
import org.apache.directory.shared.kerberos.components.HostAddress;
import org.apache.directory.shared.kerberos.components.KrbCredInfo;
import org.junit.Before;
import org.junit.Test;


/**
 * test cases for EncKrbCredPart codec
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class EncKrbCredPartDecoderTest
{
    private List<KrbCredInfo> ticketInfo;
    private int nonce;
    private KerberosTime timestamp;
    private int usec;
    private HostAddress senderAddress;
    private HostAddress recipientAddress;

    private List<FieldValueHolder> optionalFieldValueList;

    class FieldValueHolder
    {
        String fieldName;
        Object value;


        FieldValueHolder( String fieldName, Object value )
        {
            this.fieldName = fieldName;
            this.value = value;
        }


        @Override
        public String toString()
        {
            return "FieldValueHolder [fieldName=" + fieldName + ", value=" + value + "]";
        }
    }


    @Before
    public void setup() throws Exception
    {
        optionalFieldValueList = new ArrayList<FieldValueHolder>();

        ticketInfo = new ArrayList<KrbCredInfo>();
        KrbCredInfo info1 = new KrbCredInfo();
        info1.setKey( new EncryptionKey( EncryptionType.DES3_CBC_MD5, new byte[]
            { 0, 1 } ) );
        ticketInfo.add( info1 );

        KrbCredInfo info2 = new KrbCredInfo();
        info2.setKey( new EncryptionKey( EncryptionType.AES128_CTS_HMAC_SHA1_96, new byte[]
            { 2, 3 } ) );
        ticketInfo.add( info2 );

        nonce = 100;
        optionalFieldValueList.add( new FieldValueHolder( "nonce", nonce ) );

        timestamp = new KerberosTime( new Date().getTime() );
        optionalFieldValueList.add( new FieldValueHolder( "timestamp", timestamp ) );

        usec = 1;
        optionalFieldValueList.add( new FieldValueHolder( "usec", usec ) );

        senderAddress = new HostAddress( Network.LOOPBACK );
        optionalFieldValueList.add( new FieldValueHolder( "senderAddress", senderAddress ) );

        recipientAddress = new HostAddress( Network.LOOPBACK );
        optionalFieldValueList.add( new FieldValueHolder( "recipientAddress", recipientAddress ) );
    }


    @Test
    public void testEncKrbCredPart() throws Exception
    {
        int size = optionalFieldValueList.size();
        for ( int i = 0; i < size; i++ )
        {
            EncKrbCredPart expected = new EncKrbCredPart();
            expected.setTicketInfo( ticketInfo );

            Map<String, Field> EncKrbCrePartFieldNameMap = getFieldMap( expected );

            List<FieldValueHolder> presentFieldList = new ArrayList<FieldValueHolder>();

            FieldValueHolder fieldValHolder = optionalFieldValueList.get( i );
            presentFieldList.add( fieldValHolder );

            Field f = EncKrbCrePartFieldNameMap.get( Strings.toLowerCaseAscii( fieldValHolder.fieldName ) );
            f.set( expected, fieldValHolder.value );

            for ( int j = i + 1; j < size; j++ )
            {
                fieldValHolder = optionalFieldValueList.get( j );
                presentFieldList.add( fieldValHolder );
                f = EncKrbCrePartFieldNameMap.get( Strings.toLowerCaseAscii( fieldValHolder.fieldName ) );
                f.set( expected, fieldValHolder.value );
            }

            ByteBuffer stream = ByteBuffer.allocate( expected.computeLength() );
            expected.encode( stream );
            stream.flip();

            Asn1Decoder decoder = new Asn1Decoder();
            EncKrbCredPartContainer container = new EncKrbCredPartContainer( stream );

            try
            {
                decoder.decode( stream, container );
            }
            catch ( DecoderException e )
            {
                // NOTE: keep this sysout for easy debugging (no need to setup a logger)
                System.out.println( "failed sequence:\n" + expected );
                throw e;
            }

            EncKrbCredPart actual = container.getEncKrbCredPart();
            assertValues( presentFieldList, actual );
        }
    }


    @Test
    public void testKrbCredInfoWithEachOptElement() throws Exception
    {
        // algorithm:
        // start from the first mandatory element and add ONLY one OPTIONAL element and then test decoding

        int size = optionalFieldValueList.size();
        for ( int i = size - 1; i >= 0; i-- )
        {
            EncKrbCredPart expected = new EncKrbCredPart();
            expected.setTicketInfo( ticketInfo );
            Map<String, Field> encKrbCredPartFieldNameMap = getFieldMap( expected );

            List<FieldValueHolder> presentFieldList = new ArrayList<FieldValueHolder>();

            FieldValueHolder fieldValHolder = optionalFieldValueList.get( i );
            presentFieldList.add( fieldValHolder );

            Field f = encKrbCredPartFieldNameMap.get( Strings.toLowerCaseAscii( fieldValHolder.fieldName ) );
            f.set( expected, fieldValHolder.value );

            ByteBuffer stream = ByteBuffer.allocate( expected.computeLength() );
            expected.encode( stream );
            stream.flip();

            Asn1Decoder decoder = new Asn1Decoder();
            EncKrbCredPartContainer container = new EncKrbCredPartContainer( stream );

            try
            {
                decoder.decode( stream, container );
            }
            catch ( DecoderException e )
            {
                // NOTE: keep this sysout for easy debugging (no need to setup a logger)
                System.out.println( "failed sequence:\n" + expected );
                throw e;
            }

            EncKrbCredPart actual = container.getEncKrbCredPart();
            assertValues( presentFieldList, actual );
        }
    }


    /**
     * compare the values that are inserted into the EncKrbCredPart objects before encoding to
     * those that are obtained from decoded EncKrbCredPart
     * 
     * @param presentFieldList the list of values that were inserted in the EncKrbCredPart class before encoding
     * @param decoded the decoded EncKrbCredPart object 
     */
    private void assertValues( List<FieldValueHolder> presentFieldList, EncKrbCredPart decoded ) throws Exception
    {
        Map<String, Field> fieldNameMap = getFieldMap( decoded );
        for ( FieldValueHolder fh : presentFieldList )
        {
            Field actualField = fieldNameMap.get( Strings.toLowerCaseAscii( fh.fieldName ) );
            Object decodedValue = actualField.get( decoded );

            //System.out.println( fh.fieldName + " expected: " + fh.value + " , actual: " + decodedValue );

            assertTrue( decodedValue.equals( fh.value ) );
        }
    }


    /**
     * create a map with the field's name and field objects of the KrbCreInfo
     * 
     * @param source the EncKrbCredPart object
     */
    private Map<String, Field> getFieldMap( EncKrbCredPart source )
    {
        Field[] fields = source.getClass().getDeclaredFields();

        Map<String, Field> fieldNameMap = new HashMap<String, Field>();

        for ( Field f : fields )
        {
            f.setAccessible( true );
            fieldNameMap.put( Strings.toLowerCaseAscii( f.getName() ), f );
        }

        return fieldNameMap;
    }

}
