/*
 * Copyright (c) 2007, Rickard Öberg. All Rights Reserved.
 * Copyright (c) 2012, Paul Merlin. All Rights Reserved.
 *
 * Licensed 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.qi4j.test.value;

import java.io.ByteArrayOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.qi4j.api.association.Association;
import org.qi4j.api.association.ManyAssociation;
import org.qi4j.api.association.NamedAssociation;
import org.qi4j.api.common.Optional;
import org.qi4j.api.common.UseDefaults;
import org.qi4j.api.common.Visibility;
import org.qi4j.api.entity.EntityBuilder;
import org.qi4j.api.entity.EntityComposite;
import org.qi4j.api.entity.EntityReference;
import org.qi4j.api.injection.scope.Service;
import org.qi4j.api.injection.scope.This;
import org.qi4j.api.mixin.Mixins;
import org.qi4j.api.property.Property;
import org.qi4j.api.unitofwork.UnitOfWork;
import org.qi4j.api.value.ValueBuilder;
import org.qi4j.api.value.ValueComposite;
import org.qi4j.api.value.ValueSerialization;
import org.qi4j.bootstrap.AssemblyException;
import org.qi4j.bootstrap.ModuleAssembly;
import org.qi4j.test.AbstractQi4jTest;
import org.qi4j.test.EntityTestAssembler;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

/**
 * Assert that ValueSerialization behaviour on ValueComposites is correct.
 */
// TODO Assert Arrays behaviour!
// TODO Assert Generics behaviour!
public abstract class AbstractValueCompositeSerializationTest
    extends AbstractQi4jTest
{
    @Rule
    public TestName testName = new TestName();

    @Override
    public void assemble( ModuleAssembly module )
        throws AssemblyException
    {
        module.values( SomeValue.class, AnotherValue.class, FooValue.class, CustomFooValue.class,
                       SpecificCollection.class /*, SpecificValue.class, GenericValue.class */ );

        new EntityTestAssembler().visibleIn( Visibility.layer ).assemble( module.layer().module( "persistence" ) );
        module.entities( BarEntity.class );
    }

    @Before
    public void before()
    {
        module.injectTo( this );
    }

    @Service
    protected ValueSerialization valueSerialization;

    @Test
    public void givenValueCompositeWhenSerializingAndDeserializingExpectEquals()
        throws Exception
    {
        UnitOfWork uow = module.newUnitOfWork();
        try
        {
            SomeValue some = buildSomeValue();

            // Serialize using injected service
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            valueSerialization.serialize( some, output );
            String stateString = output.toString( "UTF-8" );

            // Deserialize using Module API
            SomeValue some2 = module.newValueFromSerializedState( SomeValue.class, stateString );

            assertThat( "Same value toString", some.toString(), equalTo( some2.toString() ) );
            assertThat( "Same value", some, equalTo( some2 ) );
            assertThat( "Same JSON value toString", stateString, equalTo( some2.toString() ) );
            assertThat( "Same JSON value", some.customFoo().get() instanceof CustomFooValue, is( true ) );
            assertThat( "Same JSON value explicit", some.customFooValue().get() instanceof CustomFooValue, is( true ) );

            assertThat( "String Integer Map", some2.stringIntMap().get().get( "foo" ), equalTo( 42 ) );
            assertThat( "String Value Map", some2.stringValueMap().get().get( "foo" ).internalVal(), equalTo( "Bar" ) );
            assertThat( "Nested Entities", some2.barAssociation().get().cathedral().get(), equalTo( "bazar in barAssociation" ) );
        }
        catch( Exception ex )
        {
            throw ex;
        }
        finally
        {
            uow.discard();
        }
    }

    /**
     * @return a SomeValue ValueComposite whose state is populated with test data.
     */
    private SomeValue buildSomeValue()
    {
        ValueBuilder<SomeValue> builder = module.newValueBuilder( SomeValue.class );
        SomeValue proto = builder.prototype();
        proto.anotherList().get().add( module.newValue( AnotherValue.class ) );

        ValueBuilder<SpecificCollection> specificColBuilder = module.newValueBuilder( SpecificCollection.class );
        SpecificCollection specificColProto = specificColBuilder.prototype();
        List<String> genericList = new ArrayList<>( 2 );
        genericList.add( "Some" );
        genericList.add( "String" );
        specificColProto.genericList().set( genericList );
        proto.specificCollection().set( specificColBuilder.newInstance() );

        AnotherValue anotherValue1 = createAnotherValue( "Foo", "Bar" );
        AnotherValue anotherValue2 = createAnotherValue( "Habba", "ZoutZout" );
        AnotherValue anotherValue3 = createAnotherValue( "Niclas", "Hedhman" );

        // FIXME Some Control Chars are not supported in JSON nor in XML, what should we do about it?
        // Should Zest Core ensure the chars used in strings are supported by the whole stack?
        // proto.string().set( "Foo\"Bar\"\nTest\f\t\b" );
        proto.string().set( "Foo\"Bar\"\nTest\t" );
        proto.string2().set( "/Foo/bar" );
        proto.number().set( 42L );
        proto.date().set( new Date() );
        // We specify the TimeZone explicitely here so that serialized/deserialized is equals
        // See https://github.com/JodaOrg/joda-time/issues/106
        proto.dateTime().set( new DateTime( "2020-03-04T13:24:35", DateTimeZone.forOffsetHours( 1 ) ) );
        proto.localDate().set( new LocalDate() );
        proto.localDateTime().set( new LocalDateTime() );
        proto.entityReference().set( EntityReference.parseEntityReference( "12345" ) );
        proto.stringIntMap().get().put( "foo", 42 );

        // Can't put more than one entry in Map because this test rely on the fact that the underlying implementations
        // maintain a certain order but it's not the case on some JVMs. On OpenJDK 8 they are reversed for example.
        // This should not be enforced tough as both the Map API and the JSON specification state that name-value pairs
        // are unordered.
        // As a consequence this test should be enhanced to be Map order independant.
        //
        // proto.stringIntMap().get().put( "bar", 67 );

        proto.stringValueMap().get().put( "foo", anotherValue1 );
        proto.another().set( anotherValue1 );
        // proto.arrayOfValues().set( new AnotherValue[] { anotherValue1, anotherValue2, anotherValue3 } );
        proto.serializable().set( new SerializableObject() );
        proto.foo().set( module.newValue( FooValue.class ) );
        proto.fooValue().set( module.newValue( FooValue.class ) );
        proto.customFoo().set( module.newValue( CustomFooValue.class ) );
        proto.customFooValue().set( module.newValue( CustomFooValue.class ) );

        // Arrays
        // TODO FIXME Disabled as ValueComposite equality fails here
        //proto.primitiveByteArray().set( new byte[]
        //    {
        //        9, -12, 42, -12, 127, 23, -128, 73
        //    } );
        //proto.byteArray().set( new Byte[]
        //    {
        //        9, null, -12, 23, -12, 127, -128, 73
        //    } );

        // NestedEntities
        proto.barAssociation().set( buildBarEntity( "bazar in barAssociation" ) );
        proto.barEntityAssociation().set( buildBarEntity( "bazar in barEntityAssociation" ) );
        proto.barManyAssociation().add( buildBarEntity( "bazar ONE in barManyAssociation" ) );
        proto.barManyAssociation().add( buildBarEntity( "bazar TWO in barManyAssociation" ) );
        proto.barEntityManyAssociation().add( buildBarEntity( "bazar ONE in barEntityManyAssociation" ) );
        proto.barEntityManyAssociation().add( buildBarEntity( "bazar TWO in barEntityManyAssociation" ) );
        proto.barNamedAssociation().put( "bazar", buildBarEntity( "bazar in barNamedAssociation" ) );
        proto.barNamedAssociation().put( "cathedral", buildBarEntity( "cathedral in barNamedAssociation" ) );
        proto.barEntityNamedAssociation().put( "bazar", buildBarEntity( "bazar in barEntityNamedAssociation" ) );
        proto.barEntityNamedAssociation().put( "cathedral", buildBarEntity( "cathedral in barEntityNamedAssociation" ) );

        return builder.newInstance();
    }

    private AnotherValue createAnotherValue( String val1, String val2 )
    {
        ValueBuilder<AnotherValue> valueBuilder = module.newValueBuilder( AnotherValue.class );
        valueBuilder.prototype().val1().set( val1 );
        valueBuilder.prototypeFor( AnotherValueInternalState.class ).val2().set( val2 );
        return valueBuilder.newInstance();
    }

    private BarEntity buildBarEntity( String cathedral )
    {
        EntityBuilder<BarEntity> barBuilder = module.currentUnitOfWork().newEntityBuilder( BarEntity.class );
        barBuilder.instance().cathedral().set( cathedral );
        return barBuilder.newInstance();
    }

    public enum TestEnum
    {
        somevalue, anothervalue
    }

    public interface SomeValue
        extends ValueComposite
    {
        Property<String> string();

        Property<String> string2();

        @Optional
        Property<String> nullString();

        @UseDefaults
        Property<String> emptyString();

        @UseDefaults
        Property<Long> number();

        Property<Date> date();

        Property<DateTime> dateTime();

        Property<LocalDate> localDate();

        Property<LocalDateTime> localDateTime();

        Property<EntityReference> entityReference();

        @UseDefaults
        Property<List<String>> stringList();

        @UseDefaults
        Property<Map<String, Integer>> stringIntMap();

        @UseDefaults
        Property<Map<String, AnotherValue>> stringValueMap();

        Property<AnotherValue> another();

        // Property<AnotherValue[]> arrayOfValues();

        @Optional
        Property<AnotherValue> anotherNull();

        @UseDefaults
        Property<List<AnotherValue>> anotherList();

        @Optional
        Property<List<AnotherValue>> anotherListNull();

        @UseDefaults
        Property<List<AnotherValue>> anotherListEmpty();

        @UseDefaults
        Property<TestEnum> testEnum();

        // TODO FIXME Disabled as ValueComposite equality fails here
        //Property<byte[]> primitiveByteArray();
        //
        //@Optional
        //Property<byte[]> primitiveByteArrayNull();
        //
        //Property<Byte[]> byteArray();
        //
        //@Optional
        //Property<Byte[]> byteArrayNull();

        Property<Object> serializable();

        Property<Foo> foo();

        Property<FooValue> fooValue();

        Property<Foo> customFoo();

        Property<FooValue> customFooValue();

        Property<SpecificCollection> specificCollection();

        /* Too complicated to do generics here for now
         Property<SpecificValue> specificValue();
         */
        @Optional
        Association<Bar> barAssociationOptional();

        Association<Bar> barAssociation();

        Association<BarEntity> barEntityAssociation();

        ManyAssociation<Bar> barManyAssociationEmpty();

        ManyAssociation<Bar> barManyAssociation();

        ManyAssociation<BarEntity> barEntityManyAssociation();

        NamedAssociation<Bar> barNamedAssociationEmpty();

        NamedAssociation<Bar> barNamedAssociation();

        NamedAssociation<BarEntity> barEntityNamedAssociation();
    }

    public interface SpecificCollection
        extends GenericCollection<String>
    {
    }

    public interface GenericCollection<TYPE>
        extends ValueComposite
    {
        @UseDefaults
        Property<List<TYPE>> genericList();
    }

    public interface SpecificValue
        extends GenericValue<String>
    {
    }

    public interface GenericValue<TYPE>
        extends ValueComposite
    {
        @Optional
        Property<TYPE> item();
    }

    @Mixins( AnotherValueMixin.class )
    public interface AnotherValue
        extends ValueComposite
    {
        @UseDefaults
        Property<String> val1();

        String internalVal();
    }

    public interface AnotherValueInternalState
    {
        @UseDefaults
        Property<String> val2();
    }

    public static abstract class AnotherValueMixin
        implements AnotherValue
    {
        @This
        private AnotherValueInternalState internalState;

        @Override
        public String internalVal()
        {
            return internalState.val2().get();
        }
    }

    public interface Foo
    {
        @UseDefaults
        Property<String> bar();
    }

    public interface FooValue
        extends Foo, ValueComposite
    {
    }

    public interface CustomFooValue
        extends FooValue
    {
        @UseDefaults
        Property<String> custom();
    }

    public interface Bar
    {
        @UseDefaults
        Property<String> cathedral();
    }

    public interface BarEntity
        extends Bar, EntityComposite
    {
    }

    public static class SerializableObject
        implements Serializable
    {
        private static final long serialVersionUID = 1L;
        private final String foo = "Foo";
        private final int val = 35;

        @Override
        public boolean equals( Object o )
        {
            if( this == o )
            {
                return true;
            }
            if( o == null || getClass() != o.getClass() )
            {
                return false;
            }
            SerializableObject that = (SerializableObject) o;
            return val == that.val && foo.equals( that.foo );
        }

        @Override
        public int hashCode()
        {
            int result = foo.hashCode();
            result = 31 * result + val;
            return result;
        }
    }
}
