package org.drools.reteoo;

/*
 * Copyright 2005 JBoss Inc
 * 
 * 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.
 */

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

import org.drools.Cheese;
import org.drools.DroolsTestCase;
import org.drools.FactException;
import org.drools.RuleBaseConfiguration;
import org.drools.RuleBaseFactory;
import org.drools.base.ClassObjectType;
import org.drools.base.ShadowProxy;
import org.drools.common.DefaultFactHandle;
import org.drools.common.InternalWorkingMemory;
import org.drools.common.PropagationContextImpl;
import org.drools.reteoo.ReteooBuilder.IdGenerator;
import org.drools.reteoo.builder.BuildContext;
import org.drools.rule.EntryPoint;
import org.drools.FactHandle;
import org.drools.spi.PropagationContext;

/**
 * @author mproctor
 *
 */
public class ReteTest extends DroolsTestCase {
    private ReteooRuleBase ruleBase;
    private BuildContext   buildContext;
    private EntryPointNode entryPoint;

    protected void setUp() throws Exception {
        this.ruleBase = (ReteooRuleBase) RuleBaseFactory.newRuleBase();
        this.buildContext = new BuildContext( ruleBase,
                                              ((ReteooRuleBase) ruleBase).getReteooBuilder().getIdGenerator() );
        this.entryPoint = new EntryPointNode( 0,
                                              this.ruleBase.getRete(),
                                              buildContext );
        this.entryPoint.attach();

    }

    /**
     * Tests ObjectTypeNodes are correctly added to the Rete object
     * 
     * @throws Exception
     */
    public void testObjectTypeNodes() throws Exception {
        final Rete rete = ruleBase.getRete();

        final ObjectTypeNode objectTypeNode = new ObjectTypeNode( 1,
                                                                  this.entryPoint,
                                                                  new ClassObjectType( Object.class ),
                                                                  buildContext );
        objectTypeNode.attach();

        final ObjectTypeNode stringTypeNode = new ObjectTypeNode( 2,
                                                                  this.entryPoint,
                                                                  new ClassObjectType( String.class ),
                                                                  buildContext );
        stringTypeNode.attach();

        final List<ObjectTypeNode> list = rete.getObjectTypeNodes();

        // Check the ObjectTypeNodes are correctly added to Rete
        assertEquals( 2,
                      list.size() );

        assertTrue( list.contains( objectTypeNode ) );
        assertTrue( list.contains( stringTypeNode ) );
    }

    /**
     * Tests that interfaces and parent classes for an asserted  class are  cached, for  quick future iterations
     * 
     * @throws FactException
     */
    public void testCache() throws FactException {
        final ReteooWorkingMemory workingMemory = (ReteooWorkingMemory) this.ruleBase.newStatefulSession();

        // Create a Rete network with ObjectTypeNodes for List, Collection and ArrayList
        final Rete rete = ruleBase.getRete();
        ObjectTypeNode objectTypeNode = new ObjectTypeNode( 1,
                                                            this.entryPoint,
                                                            new ClassObjectType( List.class ),
                                                            buildContext );
        objectTypeNode.attach();
        MockObjectSink sink = new MockObjectSink();
        objectTypeNode.addObjectSink( sink );

        objectTypeNode = new ObjectTypeNode( 1,
                                             this.entryPoint,
                                             new ClassObjectType( Collection.class ),
                                             buildContext );
        objectTypeNode.attach();
        sink = new MockObjectSink();
        objectTypeNode.addObjectSink( sink );

        objectTypeNode = new ObjectTypeNode( 1,
                                             this.entryPoint,
                                             new ClassObjectType( ArrayList.class ),
                                             buildContext );
        objectTypeNode.attach();
        sink = new MockObjectSink();
        objectTypeNode.addObjectSink( sink );

        // ArrayList matches all three ObjectTypeNodes
        final DefaultFactHandle h1 = new DefaultFactHandle( 1,
                                                            new ArrayList() );
        rete.assertObject( h1,
                           new PropagationContextImpl( 0,
                                                       PropagationContext.ASSERTION,
                                                       null,
                                                       null,
                                                       null ),
                           workingMemory );

        // LinkedList matches two ObjectTypeNodes        
        h1.setObject( new LinkedList() );
        rete.assertObject( h1,
                           new PropagationContextImpl( 0,
                                                       PropagationContext.ASSERTION,
                                                       null,
                                                       null,
                                                       null ),
                           workingMemory );               
        
        ClassObjectTypeConf conf = ( ClassObjectTypeConf ) workingMemory.getObjectTypeConfigurationRegistry().getObjectTypeConf( this.entryPoint.getEntryPoint(),  new ArrayList() );
        assertLength( 3,
                      conf.getObjectTypeNodes() );

        conf = ( ClassObjectTypeConf ) workingMemory.getObjectTypeConfigurationRegistry().getObjectTypeConf( this.entryPoint.getEntryPoint(), new ArrayList() );
        assertLength( 3,
                      conf.getObjectTypeNodes() );

    }

    /**
     * Test asserts correctly propagate
     * 
     * @throws Exception
     */
    public void testAssertObject() throws Exception {
        final ReteooWorkingMemory workingMemory = (ReteooWorkingMemory) this.ruleBase.newStatefulSession();

        // Create a Rete network with ObjectTypeNodes for List, Collection and ArrayList
        final Rete rete = ruleBase.getRete();
        final ObjectTypeNode objectTypeNode = new ObjectTypeNode( 1,
                                                                  this.entryPoint,
                                                                  new ClassObjectType( List.class ),
                                                                  buildContext );
        objectTypeNode.attach();
        final MockObjectSink sink1 = new MockObjectSink();
        objectTypeNode.addObjectSink( sink1 );

        // There are no String ObjectTypeNodes, make sure its not propagated

        final String string = "String";
        final DefaultFactHandle h1 = new DefaultFactHandle( 1,
                                                            string );

        rete.assertObject( h1,
                           new PropagationContextImpl( 0,
                                                       PropagationContext.ASSERTION,
                                                       null,
                                                       null,
                                                       null ),
                           workingMemory );

        assertLength( 0,
                      sink1.getAsserted() );

        // There is a List ObjectTypeNode, make sure it was propagated
        final List list = new ArrayList();
        final DefaultFactHandle h2 = new DefaultFactHandle( 1,
                                                            list );

        rete.assertObject( h2,
                           new PropagationContextImpl( 0,
                                                       PropagationContext.ASSERTION,
                                                       null,
                                                       null,
                                                       null ),
                           workingMemory );

        final List asserted = sink1.getAsserted();
        assertLength( 1,
                      asserted );

        final Object[] results = (Object[]) asserted.get( 0 );
        assertSame( list,
                    ((DefaultFactHandle) results[0]).getObject() );
    }

    public void testAssertObjectWithNoMatchingObjectTypeNode() {
        final ReteooWorkingMemory workingMemory = (ReteooWorkingMemory) this.ruleBase.newStatefulSession();

        final Rete rete = ruleBase.getRete();
        assertEquals( 0,
                      rete.getObjectTypeNodes().size() );

        List list = new ArrayList();

        workingMemory.insert( list );

        assertEquals( 2,
                      rete.getObjectTypeNodes().size() );
    }

    public void testHierarchy() {
        final ReteooWorkingMemory workingMemory = (ReteooWorkingMemory) this.ruleBase.newStatefulSession();

        final Rete rete = ruleBase.getRete();
        final IdGenerator idGenerator = ruleBase.getReteooBuilder().getIdGenerator();

        // Attach a List ObjectTypeNode
        final ObjectTypeNode listOtn = new ObjectTypeNode( idGenerator.getNextId(),
                                                           this.entryPoint,
                                                           new ClassObjectType( List.class ),
                                                           buildContext );
        listOtn.attach();

        // Will automatically create an ArrayList ObjectTypeNode
        FactHandle handle = workingMemory.insert( new ArrayList() );

        // Check we have three ObjectTypeNodes, List, ArrayList and InitialFactImpl
        assertEquals( 3,
                      rete.getObjectTypeNodes().size() );

        // double check that the List reference is the same as the one we created, i.e. engine should try and recreate it
        assertSame( listOtn,
                    rete.getObjectTypeNodes( EntryPoint.DEFAULT ).get( new ClassObjectType( List.class ) ) );

        // ArrayConf should match two ObjectTypenodes for List and ArrayList
        ClassObjectTypeConf arrayConf = ( ClassObjectTypeConf ) workingMemory.getObjectTypeConfigurationRegistry().getObjectTypeConf( this.entryPoint.getEntryPoint(), new ArrayList() );
        final ObjectTypeNode arrayOtn = arrayConf.getConcreteObjectTypeNode();
        assertEquals( 2,
                      arrayConf.getObjectTypeNodes().length );

        // Check it contains List and ArrayList
        List nodes = Arrays.asList( arrayConf.getObjectTypeNodes() );
        assertEquals( 2,
                      nodes.size() );
        assertTrue( nodes.contains( arrayOtn ) );
        assertTrue( nodes.contains( listOtn ) );

        // Nodes are there, retract the fact so we can check both nodes are populated
        workingMemory.retract( handle );

        // Add MockSinks so we can track assertions
        final MockObjectSink listSink = new MockObjectSink();
        listOtn.addObjectSink( listSink );

        final MockObjectSink arraySink = new MockObjectSink();
        listOtn.addObjectSink( arraySink );

        workingMemory.insert( new ArrayList() );
        assertEquals( 1,
                      listSink.getAsserted().size() );
        assertEquals( 1,
                      arraySink.getAsserted().size() );

        // Add a Collection ObjectTypeNode, so that we can check that the data from ArrayList is sent to it
        final ObjectTypeNode collectionOtn = new ObjectTypeNode( idGenerator.getNextId(),
                                                                 this.entryPoint,
                                                                 new ClassObjectType( Collection.class ),
                                                                 buildContext );
        final MockObjectSink collectionSink = new MockObjectSink();
        collectionOtn.addObjectSink( collectionSink );
        collectionOtn.attach( new InternalWorkingMemory[]{workingMemory} );

        assertEquals( 1,
                      collectionSink.getAsserted().size() );

        // check that ArrayListConf was updated with the new ObjectTypeNode
        nodes = Arrays.asList( arrayConf.getObjectTypeNodes() );
        assertEquals( 3,
                      nodes.size() );
        assertTrue( nodes.contains( arrayOtn ) );
        assertTrue( nodes.contains( listOtn ) );
        assertTrue( nodes.contains( collectionOtn ) );
    }

    /**
     * All objects retracted from a RootNode must be propagated to all children
     * ObjectTypeNodes.
     */
    public void testRetractObject() throws Exception {
        final ReteooWorkingMemory workingMemory = (ReteooWorkingMemory) this.ruleBase.newStatefulSession();

        // Create a Rete network with ObjectTypeNodes for List, Collection and ArrayList
        final Rete rete = ruleBase.getRete();
        final ObjectTypeNode objectTypeNode = new ObjectTypeNode( 1,
                                                                  this.entryPoint,
                                                                  new ClassObjectType( List.class ),
                                                                  buildContext );
        objectTypeNode.attach();
        final MockObjectSink sink1 = new MockObjectSink();
        objectTypeNode.addObjectSink( sink1 );

        // There are no String ObjectTypeNodes, make sure its not propagated
        final String string = "String";
        final DefaultFactHandle h1 = new DefaultFactHandle( 1,
                                                            string );

        rete.assertObject( h1,
                           new PropagationContextImpl( 0,
                                                       PropagationContext.ASSERTION,
                                                       null,
                                                       null,
                                                       null ),
                           workingMemory );
        assertLength( 0,
                      sink1.getAsserted() );
        assertLength( 0,
                      sink1.getRetracted() );

        // There is a List ObjectTypeNode, make sure it was propagated
        final List list = new ArrayList();
        final DefaultFactHandle h2 = new DefaultFactHandle( 1,
                                                            list );

        // need  to assert first, to force it to build  up the cache
        rete.assertObject( h2,
                           new PropagationContextImpl( 0,
                                                       PropagationContext.ASSERTION,
                                                       null,
                                                       null,
                                                       null ),
                           workingMemory );

        rete.retractObject( h2,
                            new PropagationContextImpl( 0,
                                                        PropagationContext.ASSERTION,
                                                        null,
                                                        null,
                                                        null ),
                            workingMemory );

        final List retracted = sink1.getRetracted();
        assertLength( 1,
                      retracted );

        final Object[] results = (Object[]) retracted.get( 0 );
        assertSame( list,
                    ((DefaultFactHandle) results[0]).getObject() );
    }

    public void testIsShadowed() {
        final ReteooWorkingMemory workingMemory = (ReteooWorkingMemory) this.ruleBase.newStatefulSession();

        // Create a Rete network with ObjectTypeNodes for List, Collection and ArrayList
        final Rete rete = ruleBase.getRete();
        final ObjectTypeNode objectTypeNode = new ObjectTypeNode( 1,
                                                                  this.entryPoint,
                                                                  new ClassObjectType( Cheese.class ),
                                                                  buildContext );
        objectTypeNode.attach();
        final MockObjectSink sink1 = new MockObjectSink();
        objectTypeNode.addObjectSink( sink1 );

        // There are no String ObjectTypeNodes, make sure its not propagated

        final Cheese cheese = new Cheese( "brie",
                                          15 );
        final DefaultFactHandle h1 = new DefaultFactHandle( 1,
                                                            cheese );

        rete.assertObject( h1,
                           new PropagationContextImpl( 0,
                                                       PropagationContext.ASSERTION,
                                                       null,
                                                       null,
                                                       null ),
                           workingMemory );

        final Object[] results = (Object[]) sink1.getAsserted().get( 0 );
    }

    public void testNotShadowed() {

        Properties properties = new Properties();
        properties.setProperty( "drools.shadowProxyExcludes",
                                "org.drools.Cheese" );
        RuleBaseConfiguration conf = new RuleBaseConfiguration( properties );
        final ReteooRuleBase ruleBase = (ReteooRuleBase) RuleBaseFactory.newRuleBase( conf );
        buildContext = new BuildContext( ruleBase,
                                         ((ReteooRuleBase) ruleBase).getReteooBuilder().getIdGenerator() );
        final ReteooWorkingMemory workingMemory = new ReteooWorkingMemory( 1,
                                                                           ruleBase );

        // Create a Rete network with ObjectTypeNodes for List, Collection and ArrayList
        final Rete rete = ruleBase.getRete();
        final EntryPointNode entryPoint = new EntryPointNode( 0,
                                                              rete,
                                                              buildContext );
        entryPoint.attach();
        
        final ObjectTypeNode objectTypeNode = new ObjectTypeNode( 1,
                                                                  entryPoint,
                                                                  new ClassObjectType( Cheese.class ),
                                                                  buildContext );
        objectTypeNode.attach();
        final MockObjectSink sink1 = new MockObjectSink();
        objectTypeNode.addObjectSink( sink1 );

        // There are no String ObjectTypeNodes, make sure its not propagated

        final Cheese cheese = new Cheese( "brie",
                                          15 );
        final DefaultFactHandle h1 = new DefaultFactHandle( 1,
                                                            cheese );

        rete.assertObject( h1,
                           new PropagationContextImpl( 0,
                                                       PropagationContext.ASSERTION,
                                                       null,
                                                       null,
                                                       null ),
                           workingMemory );

        final Object[] results = (Object[]) sink1.getAsserted().get( 0 );
        assertFalse( ((DefaultFactHandle) results[0]).getObject() instanceof ShadowProxy );
    }
}