/*
 * 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.
 */

package org.drools.reteoo;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import org.drools.DroolsTestCase;
import org.drools.RuleBaseConfiguration;
import org.drools.RuleBaseFactory;
import org.drools.common.DefaultBetaConstraints;
import org.drools.common.DefaultFactHandle;
import org.drools.common.EmptyBetaConstraints;
import org.drools.common.InternalFactHandle;
import org.drools.common.PropagationContextImpl;
import org.drools.core.util.LeftTupleList;
import org.drools.reteoo.builder.BuildContext;
import org.drools.rule.Behavior;
import org.drools.rule.ContextEntry;
import org.drools.rule.Rule;
import org.drools.spi.BetaNodeFieldConstraint;
import org.drools.spi.PropagationContext;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.*;

public class JoinNodeTest extends DroolsTestCase {
    Rule rule;
    PropagationContext context;
    ReteooWorkingMemory workingMemory;
    MockObjectSource objectSource;
    MockTupleSource tupleSource;
    MockLeftTupleSink sink;
    BetaNode node;
    BetaMemory memory;
    BetaNodeFieldConstraint constraint;

    /**
     * Setup the BetaNode used in each of the tests
     */
    @Before
    public void setUp() {
        // create mock objects
        constraint = mock(BetaNodeFieldConstraint.class);
        final ContextEntry c = mock(ContextEntry.class);

        when(constraint.createContextEntry()).thenReturn(c);

        this.rule = new Rule("test-rule");
        this.context = new PropagationContextImpl(0,
                PropagationContext.ASSERTION, null, null, null);
        this.workingMemory = new ReteooWorkingMemory(1,
                (ReteooRuleBase) RuleBaseFactory.newRuleBase());

        this.tupleSource = new MockTupleSource(4);
        this.objectSource = new MockObjectSource(4);
        this.sink = new MockLeftTupleSink();

        final RuleBaseConfiguration configuration = new RuleBaseConfiguration();

        ReteooRuleBase ruleBase = (ReteooRuleBase) RuleBaseFactory
                .newRuleBase();
        BuildContext buildContext = new BuildContext(ruleBase, ruleBase
                .getReteooBuilder().getIdGenerator());

        this.node = new JoinNode(15, this.tupleSource, this.objectSource,
                new DefaultBetaConstraints(
                        new BetaNodeFieldConstraint[] { this.constraint },
                        configuration), Behavior.EMPTY_BEHAVIOR_LIST,
                buildContext);

        this.node.addTupleSink(this.sink);

        this.memory = (BetaMemory) this.workingMemory.getNodeMemory(this.node);

        // check memories are empty
        assertEquals(0, this.memory.getLeftTupleMemory().size());
        assertEquals(0, this.memory.getRightTupleMemory().size());

    }

    @Test
    public void testAttach() throws Exception {
        when( constraint.isAllowedCachedLeft(any(ContextEntry.class), any(InternalFactHandle.class))).thenReturn(true);
        when( constraint.isAllowedCachedRight(any(LeftTupleImpl.class), any(ContextEntry.class))).thenReturn(true);

        final Field objectFfield = ObjectSource.class.getDeclaredField("sink");
        objectFfield.setAccessible(true);
        ObjectSinkPropagator objectSink = (ObjectSinkPropagator) objectFfield
                .get(this.objectSource);

        final Field tupleField = LeftTupleSource.class.getDeclaredField("sink");
        tupleField.setAccessible(true);
        LeftTupleSinkPropagator tupleSink = (LeftTupleSinkPropagator) tupleField
                .get(this.tupleSource);

        assertEquals(15, this.node.getId());

        assertNotNull(objectSink);
        assertNotNull(tupleSink);

        this.node.attach();

        objectSink = (ObjectSinkPropagator) objectFfield.get(this.objectSource);
        tupleSink = (LeftTupleSinkPropagator) tupleField.get(this.tupleSource);

        assertEquals(1, objectSink.getSinks().length);

        assertEquals(1, tupleSink.getSinks().length);

        assertSame(this.node, objectSink.getSinks()[0]);

        assertSame(this.node, tupleSink.getSinks()[0]);
    }

    @Test
    public void testMemory() {
        when( constraint.isAllowedCachedLeft(any(ContextEntry.class), any(InternalFactHandle.class))).thenReturn(true);
        when( constraint.isAllowedCachedRight(any(LeftTupleImpl.class), any(ContextEntry.class))).thenReturn(true);

        final ReteooWorkingMemory workingMemory = new ReteooWorkingMemory(1,
                (ReteooRuleBase) RuleBaseFactory.newRuleBase());

        final MockObjectSource objectSource = new MockObjectSource(1);
        final MockTupleSource tupleSource = new MockTupleSource(1);

        ReteooRuleBase ruleBase = (ReteooRuleBase) RuleBaseFactory
                .newRuleBase();
        BuildContext buildContext = new BuildContext(ruleBase, ruleBase
                .getReteooBuilder().getIdGenerator());
        final JoinNode joinNode = new JoinNode(2, tupleSource, objectSource,
                EmptyBetaConstraints.getInstance(),
                Behavior.EMPTY_BEHAVIOR_LIST, buildContext);

        final BetaMemory memory = (BetaMemory) workingMemory
                .getNodeMemory(joinNode);

        assertNotNull(memory);
    }

    /**
     * Test just tuple assertions
     *
     * @throws AssertionException
     */
    @Test
    public void testAssertTuple() throws Exception {
        when( constraint.isAllowedCachedLeft(any(ContextEntry.class), any(InternalFactHandle.class))).thenReturn(true);
        when( constraint.isAllowedCachedRight(any(LeftTupleImpl.class), any(ContextEntry.class))).thenReturn(true);

        final DefaultFactHandle f0 = new DefaultFactHandle(0, "cheese");
        final LeftTupleImpl tuple0 = new LeftTupleImpl(f0, this.node, true);

        // assert tuple, should add one to left memory
        this.node.assertLeftTuple(tuple0, this.context, this.workingMemory);
        // check memories, left memory is populated, right memory is emptys
        assertEquals(1, this.memory.getLeftTupleMemory().size());
        assertEquals(0, this.memory.getRightTupleMemory().size());

        // assert tuple, should add left memory should be 2
        final DefaultFactHandle f1 = new DefaultFactHandle(1, "cheese");
        final LeftTupleImpl tuple1 = new LeftTupleImpl(f1, this.node, true);
        this.node.assertLeftTuple(tuple1, this.context, this.workingMemory);
        assertEquals(2, this.memory.getLeftTupleMemory().size());

        LeftTuple leftTuple = ((LeftTupleList)this.memory.getLeftTupleMemory()).getFirst();

        assertEquals(tuple0, leftTuple);
        assertEquals(tuple1, leftTuple.getNext());
    }

    /**
     * Test just tuple assertions
     *
     * @throws AssertionException
     */
    @Test
    public void testAssertTupleSequentialMode() throws Exception {
        when( constraint.isAllowedCachedLeft(any(ContextEntry.class), any(InternalFactHandle.class))).thenReturn(true);
        when( constraint.isAllowedCachedRight(any(LeftTupleImpl.class), any(ContextEntry.class))).thenReturn(true);

        RuleBaseConfiguration conf = new RuleBaseConfiguration();
        conf.setSequential(true);

        this.workingMemory = new ReteooWorkingMemory(1,
                (ReteooRuleBase) RuleBaseFactory.newRuleBase(conf));

        ReteooRuleBase ruleBase = (ReteooRuleBase) RuleBaseFactory
                .newRuleBase();
        BuildContext buildContext = new BuildContext(ruleBase, ruleBase
                .getReteooBuilder().getIdGenerator());
        buildContext.setTupleMemoryEnabled(false);
        buildContext.setObjectTypeNodeMemoryEnabled(false);

        // override setup, so its working in sequential mode
        this.node = new JoinNode(
                15,
                this.tupleSource,
                this.objectSource,
                new DefaultBetaConstraints(
                        new BetaNodeFieldConstraint[] { this.constraint }, conf),
                Behavior.EMPTY_BEHAVIOR_LIST, buildContext);

        this.node.addTupleSink(this.sink);

        this.memory = (BetaMemory) this.workingMemory.getNodeMemory(this.node);

        final DefaultFactHandle f0 = new DefaultFactHandle(0, "cheese");
        final LeftTupleImpl tuple0 = new LeftTupleImpl(f0, this.node, true);

        this.node.assertObject(f0, new PropagationContextImpl(0,
                                                              PropagationContext.ASSERTION, null, null, f0), this.workingMemory);

        // assert tuple
        this.node.assertLeftTuple(tuple0, new PropagationContextImpl(0,
                                                                      PropagationContext.ASSERTION, null, null, f0), this.workingMemory);

        assertEquals(1, this.sink.getAsserted().size());

        assertNull(this.memory.getLeftTupleMemory());

        assertEquals(1, this.memory.getRightTupleMemory().size());

        assertEquals(new LeftTupleImpl(tuple0, f0.getFirstRightTuple(), this.sink,
                true), ((Object[]) this.sink.getAsserted().get(0))[0]);
    }

    /**
     * Test just object assertions
     *
     * @throws Exception
     */
    @Test
    public void testAssertObject() throws Exception {
        when( constraint.isAllowedCachedLeft(any(ContextEntry.class), any(InternalFactHandle.class))).thenReturn(true);
        when( constraint.isAllowedCachedRight(any(LeftTupleImpl.class), any(ContextEntry.class))).thenReturn(true);

        final DefaultFactHandle f0 = (DefaultFactHandle) this.workingMemory
                .insert("test0");

        // assert object, should add one to right memory
        this.node.assertObject(f0, this.context, this.workingMemory);
        assertEquals(0, this.memory.getLeftTupleMemory().size());
        assertEquals(1, this.memory.getRightTupleMemory().size());

        // check new objects/handles still assert
        final DefaultFactHandle f1 = (DefaultFactHandle) this.workingMemory
                .insert("test1");
        this.node.assertObject(f1, this.context, this.workingMemory);
        assertEquals(2, this.memory.getRightTupleMemory().size());

        RightTuple rightTuple = this.memory.getRightTupleMemory().getFirst(
                new LeftTupleImpl(f0, this.node, true), null);

        final InternalFactHandle rf0 = rightTuple.getFactHandle();
        final InternalFactHandle rf1 = ((RightTuple) rightTuple.getNext())
                .getFactHandle();

        assertEquals(f0, rf0);
        assertEquals(f1, rf1);
    }

    /**
     * Test assertion with both Objects and Tuples
     *
     * @throws Exception
     */
    @Test
    public void testAssertPropagations() throws Exception {
        when( constraint.isAllowedCachedLeft(any(ContextEntry.class), any(InternalFactHandle.class))).thenReturn(true);
        when( constraint.isAllowedCachedRight(any(LeftTupleImpl.class), any(ContextEntry.class))).thenReturn(true);

        // assert first right object
        final DefaultFactHandle f0 = (DefaultFactHandle) this.workingMemory
                .insert("test0");
        this.node.assertObject(f0, this.context, this.workingMemory);

        // assert tuple, should add left memory should be 2
        final DefaultFactHandle f1 = new DefaultFactHandle(1, "cheese");
        final LeftTupleImpl tuple1 = new LeftTupleImpl(f1, this.node, true);
        this.node.assertLeftTuple(tuple1, this.context, this.workingMemory);

        assertEquals(1, this.sink.getAsserted().size());

        assertEquals(new LeftTupleImpl(tuple1, f0.getFirstRightTuple(), this.sink,
                true), ((Object[]) this.sink.getAsserted().get(0))[0]);

        final DefaultFactHandle f2 = new DefaultFactHandle(2, "cheese");
        final LeftTupleImpl tuple2 = new LeftTupleImpl(f2, this.node, true);
        this.node.assertLeftTuple(tuple2, this.context, this.workingMemory);

        assertEquals(2, this.sink.getAsserted().size());
        assertEquals(new LeftTupleImpl(tuple2, f0.getFirstRightTuple(), this.sink,
                true), ((Object[]) this.sink.getAsserted().get(1))[0]);

        final DefaultFactHandle f3 = (DefaultFactHandle) this.workingMemory
                .insert("test2");
        this.node.assertObject(f3, this.context, this.workingMemory);

        assertEquals(4, this.sink.getAsserted().size());

        final List tuples = new ArrayList();
        tuples.add(((Object[]) this.sink.getAsserted().get(2))[0]);
        tuples.add(((Object[]) this.sink.getAsserted().get(3))[0]);

        assertTrue(tuples.contains(new LeftTupleImpl(tuple1, f3
                .getFirstRightTuple(), this.sink, true)));
        assertTrue(tuples.contains(new LeftTupleImpl(tuple2, f3
                .getFirstRightTuple(), this.sink, true)));
    }

    /**
     * Test Tuple retraction
     *
     * @throws Exception
     * @throws RetractionException
     */
    @Test
    public void testRetractTuple() throws Exception {
        when( constraint.isAllowedCachedLeft(any(ContextEntry.class), any(InternalFactHandle.class))).thenReturn(true);
        when( constraint.isAllowedCachedRight(any(LeftTupleImpl.class), any(ContextEntry.class))).thenReturn(true);

        // setup 2 tuples 3 fact handles
        final DefaultFactHandle f0 = (DefaultFactHandle) this.workingMemory
                .insert("test0");
        this.node.assertObject(f0, this.context, this.workingMemory);

        final DefaultFactHandle f1 = (DefaultFactHandle) this.workingMemory
                .insert("test1");
        final LeftTupleImpl tuple1 = new LeftTupleImpl(f1, this.node, true);
        this.node.assertLeftTuple(tuple1, this.context, this.workingMemory);

        final DefaultFactHandle f2 = (DefaultFactHandle) this.workingMemory
                .insert("test2");
        final LeftTupleImpl tuple2 = new LeftTupleImpl(f2, this.node, true);
        this.node.assertLeftTuple(tuple2, this.context, this.workingMemory);

        final DefaultFactHandle f3 = (DefaultFactHandle) this.workingMemory
                .insert("test3");
        this.node.assertObject(f3, this.context, this.workingMemory);

        final DefaultFactHandle f4 = (DefaultFactHandle) this.workingMemory
                .insert("test4");
        this.node.assertObject(f4, this.context, this.workingMemory);

        assertLength(6, this.sink.getAsserted());

        // Double check the item is in memory
        final BetaMemory memory = (BetaMemory) this.workingMemory
                .getNodeMemory(this.node);
        assertTrue(memory.getRightTupleMemory().contains(
                f0.getFirstRightTuple()));

        // Retract an object, check propagations and memory
        this.node.retractRightTuple(f0.getFirstRightTuple(), this.context,
                this.workingMemory);
        assertLength(2, this.sink.getRetracted());

        List tuples = new ArrayList();
        tuples.add(((Object[]) this.sink.getRetracted().get(0))[0]);
        tuples.add(((Object[]) this.sink.getRetracted().get(1))[0]);

        assertTrue(tuples.contains(new LeftTupleImpl(tuple1, f0
                .getFirstRightTuple(), this.sink, true)));
        assertTrue(tuples.contains(new LeftTupleImpl(tuple1, f0
                .getFirstRightTuple(), this.sink, true)));

        // Now check the item is no longer in memory
        assertFalse(memory.getRightTupleMemory().contains(
                f0.getFirstRightTuple()));

        this.node.retractLeftTuple(tuple2, this.context, this.workingMemory);
        assertEquals(4, this.sink.getRetracted().size());

        tuples = new ArrayList();
        tuples.add(((Object[]) this.sink.getRetracted().get(2))[0]);
        tuples.add(((Object[]) this.sink.getRetracted().get(3))[0]);

        assertTrue(tuples.contains(new LeftTupleImpl(tuple2, f3
                .getFirstRightTuple(), this.sink, true)));
        assertTrue(tuples.contains(new LeftTupleImpl(tuple2, f4
                .getFirstRightTuple(), this.sink, true)));
    }

    @Test
    public void testConstraintPropagations() throws Exception {
        when( constraint.isAllowedCachedLeft(any(ContextEntry.class), any(InternalFactHandle.class))).thenReturn(false);
        when( constraint.isAllowedCachedRight(any(LeftTupleImpl.class), any(ContextEntry.class))).thenReturn(false);

        // assert first right object
        final DefaultFactHandle f0 = (DefaultFactHandle) this.workingMemory
                .insert("test0");
        this.node.assertObject(f0, this.context, this.workingMemory);

        // assert tuple, should add left memory should be 2
        final DefaultFactHandle f1 = new DefaultFactHandle(1, "cheese");
        final LeftTupleImpl tuple1 = new LeftTupleImpl(f1, this.node, true);
        this.node.assertLeftTuple(tuple1, this.context, this.workingMemory);

        // Should be no assertions
        assertLength(0, this.sink.getAsserted());

        this.node.retractRightTuple(f0.getFirstRightTuple(), this.context,
                this.workingMemory);
        assertLength(0, this.sink.getRetracted());
    }

    @Test
    public void testUpdateSink() {
        when( constraint.isAllowedCachedLeft(any(ContextEntry.class), any(InternalFactHandle.class))).thenReturn(true);
        when( constraint.isAllowedCachedRight(any(LeftTupleImpl.class), any(ContextEntry.class))).thenReturn(true);

        final ReteooWorkingMemory workingMemory = new ReteooWorkingMemory(1,
                (ReteooRuleBase) RuleBaseFactory.newRuleBase());

        ReteooRuleBase ruleBase = (ReteooRuleBase) RuleBaseFactory
                .newRuleBase();
        BuildContext buildContext = new BuildContext(ruleBase, ruleBase
                .getReteooBuilder().getIdGenerator());

        final JoinNode joinNode = new JoinNode(1, this.tupleSource,
                this.objectSource, EmptyBetaConstraints.getInstance(),
                Behavior.EMPTY_BEHAVIOR_LIST, buildContext);

        // Add the first tuple sink and assert a tuple and object
        // The sink has no memory
        final MockLeftTupleSink sink1 = new MockLeftTupleSink(2);
        joinNode.addTupleSink(sink1);

        final DefaultFactHandle f0 = new DefaultFactHandle(0, "string0");

        final LeftTupleImpl tuple1 = new LeftTupleImpl(f0, this.node, true);

        joinNode.assertLeftTuple(tuple1, this.context, workingMemory);

        final String string1 = "string1";
        final DefaultFactHandle string1Handle = new DefaultFactHandle(1,
                string1);

        joinNode.assertObject(string1Handle, this.context, workingMemory);

        assertLength(1, sink1.getAsserted());

        // Add the new sink, this should be updated from the re-processed
        // joinnode memory
        final MockLeftTupleSink sink2 = new MockLeftTupleSink(3);
        assertLength(0, sink2.getAsserted());

        joinNode.updateSink(sink2, this.context, workingMemory);

        assertLength(1, sink2.getAsserted());
    }

}
