/*
 * Copyright 2010 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.test.dsl;

import static org.mockito.Matchers.any;
import static org.mockito.Matchers.same;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.drools.common.InternalFactHandle;
import org.drools.common.InternalWorkingMemory;
import org.drools.reteoo.LeftTuple;
import org.drools.reteoo.LeftTuple;
import org.drools.reteoo.LeftTupleSink;
import org.drools.reteoo.ObjectSinkNode;
import org.drools.reteoo.RightTuple;
import org.drools.reteoo.RightTupleSink;
import org.drools.reteoo.Sink;
import org.drools.spi.PropagationContext;
import org.hamcrest.CoreMatchers;
import org.junit.Assert;
import org.junit.matchers.JUnitMatchers;
import org.mockito.ArgumentCaptor;
import org.mockito.exceptions.base.MockitoAssertionError;
import org.mockito.internal.verification.api.VerificationMode;
import org.mvel2.MVEL;
import org.mvel2.ParserConfiguration;
import org.mvel2.ParserContext;
import org.mvel2.PropertyAccessException;

/**
 * A helper class to process mock related commands in the DSL
 */
public class MockitoHelper {

    public static void process(DslStep step,
                               Sink node,
                               Map<String, Object> context,
                               InternalWorkingMemory wm) {
        try {
            List<String[]> cmds = step.getCommands();

            for ( String[] cmd : cmds ) {
                if ( cmd[0].equals( "verify" ) ) {
                    if ( "count".equals( cmd[2] ) ) {
                        verifyCount( step,
                                     node,
                                     wm,
                                     cmd,
                                     context );
                    } else if ( "exec".equals( cmd[2] ) ) {
                        verifyExec( step,
                                    node,
                                    wm,
                                    cmd,
                                    context );
                    } else if ( cmd.length == 3 || cmd.length == 4 ) {
                        verifyExpression( step,
                                          node,
                                          wm,
                                          cmd,
                                          context );
                    } else {
                        throw new IllegalArgumentException( "line " + step.getLine() + ": command '" + cmd[2] + "' does not exist in " + Arrays.toString( cmd ) );
                    }
                } else {
                    throw new IllegalArgumentException( "line " + step.getLine() + ": command does not exist " + Arrays.toString( cmd ) );
                }
            }
        } catch ( Exception e ) {
            throw new IllegalArgumentException( "line " + step.getLine() + ": unable to execute step " + step,
                                                e );
        }
    }

    @SuppressWarnings("unchecked")
    private static void verifyCount(DslStep step,
                                    Sink node,
                                    InternalWorkingMemory wm,
                                    String[] cmd,
                                    Map<String, Object> context) throws AssertionError {
        int times = Integer.valueOf( cmd[3] );
        VerificationMode counter;
        if ( times >= 0 ) {
            counter = times( times );
        } else {
            counter = atLeastOnce();
        }
        try {
            ArgumentCaptor< ? > captor = null;
            if ( "assertLeft".equals( cmd[1] ) ) {
                captor = ArgumentCaptor.forClass( LeftTuple.class );
                verify( (LeftTupleSink) node,
                        counter ).assertLeftTuple( ((ArgumentCaptor<LeftTuple>) captor).capture(),
                                                   any( PropagationContext.class ),
                                                   same( wm ) );
            } else if ( "retractLeft".equals( cmd[1] ) ) {
                captor = ArgumentCaptor.forClass( LeftTuple.class );
                verify( (LeftTupleSink) node,
                        counter ).retractLeftTuple( ((ArgumentCaptor<LeftTuple>) captor).capture(),
                                                    any( PropagationContext.class ),
                                                    same( wm ) );
            } else if ( "modifyLeft".equals( cmd[1] ) ) {
                captor = ArgumentCaptor.forClass( LeftTuple.class );
                verify( (LeftTupleSink) node,
                        counter ).modifyLeftTuple( ((ArgumentCaptor<LeftTuple>) captor).capture(),
                                                   any( PropagationContext.class ),
                                                   same( wm ) );
            } else if ( "assertRight".equals( cmd[1] ) ) {
                captor = ArgumentCaptor.forClass( InternalFactHandle.class );
                verify( (ObjectSinkNode) node,
                        counter ).assertObject( ((ArgumentCaptor<InternalFactHandle>) captor).capture(),
                                                any( PropagationContext.class ),
                                                same( wm ) );
            } else if ( "modifyRight".equals( cmd[1] ) ) {
                captor = ArgumentCaptor.forClass( RightTuple.class );
                verify( (RightTupleSink) node,
                        counter ).modifyRightTuple( ((ArgumentCaptor<RightTuple>) captor).capture(),
                                                    any( PropagationContext.class ),
                                                    same( wm ) );
            } else if ( "retractRight".equals( cmd[1] ) ) {
                captor = ArgumentCaptor.forClass( RightTuple.class );
                verify( (RightTupleSink) node,
                        counter ).retractRightTuple( ((ArgumentCaptor<RightTuple>) captor).capture(),
                                                     any( PropagationContext.class ),
                                                     same( wm ) );
            } else {
                throw new IllegalArgumentException( "line " + step.getLine() + ": command does not exist " + Arrays.toString( cmd ) );
            }
            String key = getCaptorKey( node,
                                       cmd );
            context.put( key,
                         captor );
        } catch ( MockitoAssertionError e ) {
            AssertionError ae = new AssertionError( "line " + step.getLine() + ": verify failed: " + e.getMessage() );
            ae.setStackTrace( e.getStackTrace() );
            throw ae;
        }
    }

    private static String getCaptorKey(Sink node,
                                       String[] cmd) {
        return System.identityHashCode( node ) + "." + cmd[1] + ".captor";
    }

    private static void verifyExpression(DslStep step,
                                         Sink node,
                                         InternalWorkingMemory wm,
                                         String[] cmd,
                                         Map<String, Object> context) throws AssertionError {
        // check that the captor already exists:
        String key = getCaptorKey( node,
                                   cmd );
        ArgumentCaptor<?> captor = (ArgumentCaptor<?>) context.get( key );
        if ( captor == null ) {
            // create the captor
            verifyCount( step,
                         node,
                         wm,
                         new String[]{"verify", cmd[1], "count", "-1"},
                         context );
            captor = (ArgumentCaptor<?>) context.get( key );
        }

        // create a map with all captured tuples as variables
        Map<String, Object> vars = new HashMap<String, Object>();
        populateCapturedArguments( captor,
                                   vars );
        // add all context variables, just in case
        vars.putAll( context );

        // add the static imports for hamcrest matchers
        ParserConfiguration pconf = new ParserConfiguration();
        addStaticImports( pconf,
                          CoreMatchers.class );
        addStaticImports( pconf,
                          JUnitMatchers.class );
        try {
            pconf.addImport( "isTuple", IsTuple.class.getMethod( "isTuple", List.class ) );
        } catch ( Exception e1 ) {
            e1.printStackTrace();
        }
        // add import for JUnit assert class
        pconf.addImport( "Assert",
                         Assert.class );

        // compile MVEL expression
        ParserContext mvelctx = new ParserContext( pconf );
        String expression;
        if ( cmd.length == 3 ) {
            expression = "Assert.assertTrue( " + cmd[2].replaceAll( "h(\\d+)",
                                                                    "Handles[$1]" ) + " );";
        } else {
            String val = cmd[2].replaceAll( "h(\\d+)",
                                            "Handles[$1]" );
            String matcher = cmd[3].replaceAll( "h(\\d+)",
                                                "Handles[$1]" );
            expression = "Assert.assertThat( " + val + ", " + matcher + " );";
        }
        try {
            Serializable compiled = MVEL.compileExpression( expression,
                                                            mvelctx );

            // execute the expression
            MVEL.executeExpression( compiled,
                                    vars );
        } catch ( PropertyAccessException e ) {
            String message;
            if ( e.getCause() instanceof InvocationTargetException ) {
                message = ((InvocationTargetException) e.getCause()).getTargetException().toString();
            } else {
                message = e.getMessage();
            }

            Assert.fail( "[ERROR] line " + step.getLine() + " - Executing expression: '" + expression + "'\n" + message );
        }
    }

    private static void populateCapturedArguments(ArgumentCaptor< ? > captor,
                                                  Map<String, Object> vars) {
        int i = 0;
        for ( Object obj : captor.getAllValues() ) {
            if( obj instanceof LeftTuple ) {
                vars.put( "tuple" + (i++),
                          ((LeftTuple)obj).toFactHandles() );
            } else if( obj instanceof RightTuple ) {
                vars.put( "handle" + (i++),
                          ((RightTuple)obj).getFactHandle() );
            } else if( obj instanceof InternalFactHandle ) {
                vars.put( "handle" + (i++),
                          obj );
            }
        }
    }

    private static void verifyExec(DslStep step,
                                   Sink node,
                                   InternalWorkingMemory wm,
                                   String[] cmd,
                                   Map<String, Object> context) throws AssertionError {
        // check that the captor already exists:
        String key = getCaptorKey( node,
                                   cmd );
        ArgumentCaptor<?> captor = (ArgumentCaptor<?>) context.get( key );
        if ( captor == null ) {
            // create the captor
            verifyCount( step,
                         node,
                         wm,
                         new String[]{"verify", cmd[1], "count", "-1"},
                         context );
            captor = (ArgumentCaptor<?>) context.get( key );
        }

        // create a map with all captured tuples as variables
        Map<String, Object> vars = new HashMap<String, Object>();
        populateCapturedArguments( captor,
                                   vars );
        // add all context variables, just in case
        vars.putAll( context );

        // add the static imports for hamcrest matchers
        ParserConfiguration pconf = new ParserConfiguration();
        addStaticImports( pconf,
                          CoreMatchers.class );
        addStaticImports( pconf,
                          JUnitMatchers.class );
        try {
            pconf.addImport( "isTuple", IsTuple.class.getMethod( "isTuple", List.class ) );
        } catch ( Exception e1 ) {
            e1.printStackTrace();
        }
        // add import for JUnit assert class
        pconf.addImport( "Assert",
                         Assert.class );

        // compile MVEL expression
        ParserContext mvelctx = new ParserContext( pconf );
        String expression = cmd[3].replaceAll( "h(\\d+)",
                                               "Handles[$1]" );
        try {
            Serializable compiled = MVEL.compileExpression( expression,
                                                            mvelctx );
            // execute the expression
            MVEL.executeExpression( compiled,
                                    vars );
        } catch ( PropertyAccessException e ) {
            String message;
            if ( e.getCause() instanceof InvocationTargetException ) {
                message = ((InvocationTargetException) e.getCause()).getTargetException().toString();
            } else {
                message = e.getMessage();
            }

            Assert.fail( "[ERROR] line " + step.getLine() + " - Executing expression: '" + expression + "'\n" + message );
        }
    }

    private static void addStaticImports(ParserConfiguration pconf,
                                         Class< ? > clazz) {
        for ( Method m : clazz.getMethods() ) {
            if ( Modifier.isStatic( m.getModifiers() ) ) {
                pconf.addImport( m.getName(),
                                 m );
            }
        }
    }

}
