/*
 *  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.api.ldap.model.entry;


import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;

import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.LdapSyntax;
import org.apache.directory.api.ldap.model.schema.MatchingRule;
import org.apache.directory.api.ldap.model.schema.Normalizer;
import org.apache.directory.api.ldap.model.schema.PrepareString;
import org.apache.directory.api.ldap.model.schema.comparators.ByteArrayComparator;
import org.apache.directory.api.ldap.model.schema.syntaxCheckers.OctetStringSyntaxChecker;
import org.apache.directory.api.util.Strings;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;


/**
 * Tests that the Value class works properly as expected.
 *
 * Some notes while conducting tests:
 *
 * <ul>
 *   <li>comparing values with different types - how does this behave</li>
 *   <li>exposing access to at from value or to a comparator?</li>
 * </ul>
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
@Execution(ExecutionMode.CONCURRENT)
public class BinaryValueAttributeTypeTest
{
    private LdapSyntax s;
    private AttributeType at;
    private MatchingRule mr;

    private static final byte[] BYTES1 = new byte[]
        { 0x01, 0x02, 0x03, 0x04 };
    private static final byte[] BYTES2 = new byte[]
        { ( byte ) 0x81, ( byte ) 0x82, ( byte ) 0x83, ( byte ) 0x84 };


    /**
     * Initialize an AttributeType and the associated MatchingRule
     * and Syntax
     */
    @BeforeEach
    public void initAT()
    {
        s = EntryUtils.syntaxFactory( "1.1.1.1", false );
        s.setSyntaxChecker( OctetStringSyntaxChecker.INSTANCE );
        mr = EntryUtils.matchingRuleFactory( "1.1.2.1" );
        mr.setSyntax( s );

        mr.setLdapComparator( new ByteArrayComparator( "1.1.1" ) );
        mr.setNormalizer( new Normalizer( "1.1.1" )
        {
            public static final long serialVersionUID = 1L;

            public String normalize( String value ) throws LdapException
            {
                return normalize( value, PrepareString.AssertionType.ATTRIBUTE_VALUE );
            }
            

            public String normalize( String value, PrepareString.AssertionType assertionType ) throws LdapException
            {
                byte[] val = Strings.getBytesUtf8( value );
                // each byte will be changed to be > 0, and spaces will be trimmed
                byte[] newVal = new byte[val.length];
                int i = 0;

                for ( byte b : val )
                {
                    newVal[i++] = ( byte ) ( b & 0x007F );
                }

                return Strings.utf8ToString( Strings.trim( newVal ) );
            }
        } );

        at = new AttributeType( "1.1.3.1" );
        at.setEquality( mr );
        at.setOrdering( mr );
        at.setSubstring( mr );
        at.setSyntax( s );
    }


    /**
     * Serialize a Value
     */
    private ByteArrayOutputStream serializeValue( Value value ) throws IOException
    {
        ObjectOutputStream oOut = null;
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try
        {
            oOut = new ObjectOutputStream( out );
            value.writeExternal( oOut );
        }
        catch ( IOException ioe )
        {
            throw ioe;
        }
        finally
        {
            try
            {
                if ( oOut != null )
                {
                    oOut.flush();
                    oOut.close();
                }
            }
            catch ( IOException ioe )
            {
                throw ioe;
            }
        }

        return out;
    }


    /**
     * Deserialize a Value
     */
    private Value deserializeValue( ByteArrayOutputStream out, AttributeType at ) throws IOException,
        ClassNotFoundException
    {
        ObjectInputStream oIn = null;
        ByteArrayInputStream in = new ByteArrayInputStream( out.toByteArray() );

        try
        {
            oIn = new ObjectInputStream( in );

            Value value = Value.createValue( ( AttributeType ) null );
            value.readExternal( oIn );

            return value;
        }
        finally
        {
            try
            {
                if ( oIn != null )
                {
                    oIn.close();
                }
            }
            catch ( IOException ioe )
            {
                throw ioe;
            }
        }
    }


    /**
     * Test the constructor with bad AttributeType
     */
    @Test
    public void testBadConstructor()
    {
        // create a AT with no syntax
        AttributeType attribute = new AttributeType( "1.1.3.1" );

        Value value = Value.createValue( attribute );
        assertTrue( value.isHumanReadable() );
    }


    /**
     * Test the constructor with a null value
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testServerBinaryValueNullValue() throws LdapInvalidAttributeValueException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();

        Value value = new Value( attribute, ( byte[] ) null );

        assertNull( value.getBytes() );
        assertTrue( value.isNull() );
    }


    /**
     * Test the constructor with an empty value
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testServerBinaryValueEmptyValue() throws LdapInvalidAttributeValueException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();

        Value value = new Value( attribute, Strings.EMPTY_BYTES );

        assertTrue( Arrays.equals( Strings.EMPTY_BYTES, value.getBytes() ) );
        assertFalse( value.isNull() );
    }


    /**
     * Test the constructor with a value
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testServerBinaryValueNoValue() throws LdapInvalidAttributeValueException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();
        byte[] val = new byte[]
            { 0x01 };
        Value bv = new Value( attribute, val );
        assertTrue( Arrays.equals( val, bv.getBytes() ) );
        assertFalse( bv.isNull() );
    }


    /**
     * Test the constructor with a value
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testServerBinaryValue() throws LdapInvalidAttributeValueException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();
        byte[] val = new byte[]
            { 0x01 };
        Value value = new Value( attribute, val );

        assertTrue( Arrays.equals( val, value.getBytes() ) );
        assertFalse( value.isNull() );
        assertTrue( Arrays.equals( val, value.getBytes() ) );
    }


    /**
     * Test the clone method
     * 
     * @throws LdapException If the test failed
     */
    @Test
    public void testClone() throws LdapException
    {
        AttributeType at1 = EntryUtils.getBytesAttributeType();
        Value bv = new Value( at1, ( byte[] ) null );
        Value bv1 = bv.clone();

        assertEquals( bv, bv1 );

        bv = new Value( Strings.EMPTY_BYTES );

        assertNotSame( bv, bv1 );
        assertTrue( Arrays.equals( Strings.EMPTY_BYTES, bv.getBytes() ) );

        bv = new Value( at, BYTES2 );
        bv1 = bv.clone();

        assertEquals( bv, bv1 );
    }


    /**
     * Test the equals method
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testEquals() throws LdapInvalidAttributeValueException
    {
        AttributeType at1 = EntryUtils.getBytesAttributeType();

        Value value1 = new Value( at1, new byte[]
            { 0x01, ( byte ) 0x02 } );
        Value value2 = new Value( at1, new byte[]
            { 0x01, ( byte ) 0x02 } );
        Value value3 = new Value( at1, new byte[]
            { 0x01, ( byte ) 0x82 } );
        Value value4 = new Value( at1, new byte[]
            { 0x01 } );
        Value value5 = new Value( at1, ( byte[] ) null );
        Value value6 = new Value( at, new byte[]
            { 0x01, 0x02 } );
        Value value7 = new Value( EntryUtils.getIA5StringAttributeType(),
            "test" );

        assertTrue( value1.equals( value1 ) );
        assertTrue( value1.equals( value2 ) );
        assertFalse( value1.equals( value3 ) );
        assertFalse( value1.equals( value4 ) );
        assertFalse( value1.equals( value5 ) );
        assertFalse( value1.equals( "test" ) );
        assertFalse( value1.equals( null ) );

        assertTrue( value1.equals( value6 ) );
        assertFalse( value1.equals( value7 ) );
    }


    /**
     * Test the getNormValue method
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testGetNormalizedValue() throws LdapInvalidAttributeValueException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();

        Value value = new Value( attribute, ( byte[] ) null );
        assertNull( value.getBytes() );

        value = new Value( attribute, Strings.EMPTY_BYTES );
        assertTrue( Arrays.equals( Strings.EMPTY_BYTES, value.getBytes() ) );

        value = new Value( attribute, BYTES2 );
        assertTrue( Arrays.equals( BYTES2, value.getBytes() ) );
    }


    /**
     * Test the getNormValue method
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testGetNormalizedValueCopy() throws LdapInvalidAttributeValueException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();

        Value value = new Value( attribute, ( byte[] ) null );
        assertNull( value.getBytes() );

        value = new Value( attribute, Strings.EMPTY_BYTES );
        assertTrue( Arrays.equals( Strings.EMPTY_BYTES, value.getBytes() ) );

        value = new Value( attribute, BYTES2 );
        assertTrue( Arrays.equals( BYTES2, value.getBytes() ) );
    }


    /**
     * Test the getNormValue method
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testGetNormalizedValueReference() throws LdapInvalidAttributeValueException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();

        Value value = new Value( attribute, ( byte[] ) null );
        assertNull( value.getBytes() );

        value = new Value( attribute, Strings.EMPTY_BYTES );
        assertTrue( Arrays.equals( Strings.EMPTY_BYTES, value.getBytes() ) );

        value = new Value( attribute, BYTES2 );
        assertTrue( Arrays.equals( BYTES2, value.getBytes() ) );
    }


    /**
     * Test the getAttributeType method
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testgetAttributeType() throws LdapInvalidAttributeValueException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();
        Value sbv = Value.createValue( attribute );

        assertEquals( attribute, sbv.getAttributeType() );
    }


    /**
     * Test the isValid method
     * 
     * The SyntaxChecker does not accept values longer than 5 chars.
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testIsValid() throws LdapInvalidAttributeValueException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();

        new Value( attribute, ( byte[] ) null );
        new Value( attribute, Strings.EMPTY_BYTES );
        new Value( attribute, new byte[]
            { 0x01, 0x02 } );

        try
        {
            new Value( attribute, new byte[]
                { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 } );
            fail();
        }
        catch ( LdapInvalidAttributeValueException liave )
        {
            assertTrue( true );
        }
    }


    /**
     * Tests to make sure the hashCode method is working properly.
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testHashCode() throws LdapInvalidAttributeValueException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();
        Value v0 = new Value( attribute, new byte[]
            { 0x01, 0x02 } );
        Value v1 = new Value( attribute, new byte[]
            { ( byte ) 0x81, ( byte ) 0x82 } );
        Value v2 = new Value( attribute, new byte[]
            { 0x01, 0x02 } );
        assertNotSame( v0.hashCode(), v1.hashCode() );
        assertNotSame( v1.hashCode(), v2.hashCode() );
        assertEquals( v0.hashCode(), v2.hashCode() );
        assertNotSame( v0, v1 );
        assertEquals( v0, v2 );
        assertNotSame( v1, v2 );

        Value v3 = new Value( attribute, new byte[]
            { 0x01, 0x03 } );
        assertFalse( v3.equals( v0 ) );
        assertFalse( v3.equals( v1 ) );
        assertFalse( v3.equals( v2 ) );
    }


    /**
     * Test the instanceOf method
     * 
     * @throws LdapException If the test failed
     */
    @Test
    public void testInstanceOf() throws LdapException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();
        Value sbv = Value.createValue( attribute );

        assertTrue( sbv.isInstanceOf( attribute ) );

        attribute = EntryUtils.getIA5StringAttributeType();

        assertFalse( sbv.isInstanceOf( attribute ) );
    }


    /**
     * Test the normalize method
     * 
     * @throws LdapException If the test failed
     */
    @Test
    public void testNormalize() throws LdapException
    {
        AttributeType attribute = EntryUtils.getBytesAttributeType();
        Value bv = Value.createValue( attribute );

        assertEquals( null, bv.getBytes() );

        bv = new Value( attribute, Strings.EMPTY_BYTES );
        assertTrue( Arrays.equals( Strings.EMPTY_BYTES, bv.getBytes() ) );

        bv = new Value( attribute, BYTES2 );
        assertTrue( Arrays.equals( BYTES2, bv.getBytes() ) );
    }


    /**
     * Test the compareTo method
     * 
     * @throws LdapInvalidAttributeValueException If the test failed
     */
    @Test
    public void testCompareTo() throws LdapInvalidAttributeValueException
    {
        AttributeType at1 = EntryUtils.getBytesAttributeType();
        Value v0 = new Value( at1, BYTES1 );
        Value v1 = new Value( at1, BYTES1 );

        assertEquals( 0, v0.compareTo( v1 ) );
        assertEquals( 0, v1.compareTo( v0 ) );

        Value v2 = new Value( at1, ( byte[] ) null );

        assertEquals( 1, v0.compareTo( v2 ) );
        assertEquals( -1, v2.compareTo( v0 ) );
    }


    /**
     * Test serialization of a Value which normalized value is the same
     * than the value
     * 
     * @throws LdapException If the test failed
     * @throws IOException If the test failed
     * @throws ClassNotFoundException If the test failed
     */
    @Test
    public void testNormalizedBinarySameValueSerialization() throws LdapException, IOException, ClassNotFoundException
    {
        byte[] v1 = Strings.getBytesUtf8( "Test   Test" );

        // First check with a value which will be normalized
        Value sbv = new Value( at, v1 );

        Value sbvSer = deserializeValue( serializeValue( sbv ), at );

        assertEquals( sbv, sbvSer );
    }


    /**
     * Test serialization of a Value which does not have a normalized value
     * 
     * @throws LdapException If the test failed
     * @throws IOException If the test failed
     * @throws ClassNotFoundException If the test failed
     */
    @Test
    public void testNoNormalizedBinaryValueSerialization() throws LdapException, IOException, ClassNotFoundException
    {
        byte[] v1 = Strings.getBytesUtf8( "test" );
        byte[] v1Norm = Strings.getBytesUtf8( "test" );

        // First check with a value which will be normalized
        Value sbv = new Value( at, v1 );

        byte[] normalized = sbv.getBytes();

        assertTrue( Arrays.equals( v1Norm, normalized ) );
        assertTrue( Arrays.equals( v1, sbv.getBytes() ) );

        Value sbvSer = deserializeValue( serializeValue( sbv ), at );

        assertEquals( sbv, sbvSer );
    }


    /**
     * Test serialization of a null Value
     * 
     * @throws LdapException If the test failed
     * @throws IOException If the test failed
     * @throws ClassNotFoundException If the test failed
     */
    @Test
    public void testNullBinaryValueSerialization() throws LdapException, IOException, ClassNotFoundException
    {
        // First check with a value which will be normalized
        Value sbv = Value.createValue( at );

        byte[] normalized = sbv.getBytes();

        assertEquals( null, normalized );
        assertEquals( "", sbv.getString() );

        Value sbvSer = deserializeValue( serializeValue( sbv ), at );

        assertEquals( sbv, sbvSer );
    }


    /**
     * Test serialization of an empty Value
     * 
     * @throws LdapException If the test failed
     * @throws IOException If the test failed
     * @throws ClassNotFoundException If the test failed
     */
    @Test
    public void testEmptyBinaryValueSerialization() throws LdapException, IOException, ClassNotFoundException
    {
        // First check with a value which will be normalized
        Value sbv = new Value( at, Strings.EMPTY_BYTES );

        byte[] normalized = sbv.getBytes();

        assertTrue( Arrays.equals( Strings.EMPTY_BYTES, normalized ) );
        assertTrue( Arrays.equals( Strings.EMPTY_BYTES, sbv.getBytes() ) );

        Value sbvSer = deserializeValue( serializeValue( sbv ), at );

        assertEquals( sbv, sbvSer );
    }


    /**
     * Test serialization of a Value which is the same than the value
     * 
     * @throws LdapException If the test failed
     * @throws IOException If the test failed
     * @throws ClassNotFoundException If the test failed
     */
    @Test
    public void testSameNormalizedBinaryValueSerialization() throws LdapException, IOException, ClassNotFoundException
    {
        byte[] v1 = Strings.getBytesUtf8( "test" );
        byte[] v1Norm = Strings.getBytesUtf8( "test" );

        // First check with a value which will be normalized
        Value sbv = new Value( at, v1 );

        byte[] normalized = sbv.getBytes();

        assertTrue( Arrays.equals( v1Norm, normalized ) );
        assertTrue( Arrays.equals( v1, sbv.getBytes() ) );

        Value sbvSer = deserializeValue( serializeValue( sbv ), at );

        assertEquals( sbv, sbvSer );
    }
}
