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

import static org.mvel2.MVEL.eval;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.drools.ClockType;
import org.drools.FactHandle;
import org.drools.RuleBase;
import org.drools.SessionConfiguration;
import org.drools.base.ClassTypeResolver;
import org.drools.base.TypeResolver;
import org.drools.common.InternalRuleBase;
import org.drools.common.InternalWorkingMemory;
import org.drools.ide.common.client.modeldriven.testing.ActivateRuleFlowGroup;
import org.drools.ide.common.client.modeldriven.testing.CallFieldValue;
import org.drools.ide.common.client.modeldriven.testing.CallMethod;
import org.drools.ide.common.client.modeldriven.testing.ExecutionTrace;
import org.drools.ide.common.client.modeldriven.testing.Expectation;
import org.drools.ide.common.client.modeldriven.testing.FactData;
import org.drools.ide.common.client.modeldriven.testing.FieldData;
import org.drools.ide.common.client.modeldriven.testing.Fixture;
import org.drools.ide.common.client.modeldriven.testing.RetractFact;
import org.drools.ide.common.client.modeldriven.testing.Scenario;
import org.drools.ide.common.client.modeldriven.testing.VerifyFact;
import org.drools.ide.common.client.modeldriven.testing.VerifyField;
import org.drools.ide.common.client.modeldriven.testing.VerifyRuleFired;
import org.drools.ide.common.server.util.ScenarioXMLPersistence;
import org.drools.rule.Package;
import org.drools.spi.Activation;
import org.drools.time.impl.PseudoClockScheduler;
import org.mvel2.MVEL;

/**
 * This actually runs the test scenarios.
 */
public class ScenarioRunner {

    private final Scenario scenario;
    private final Map<String, Object> populatedData = new HashMap<String, Object>();
    private final Map<String, Object> globalData = new HashMap<String, Object>();
    private final Map<String, FactHandle> factHandles = new HashMap<String, FactHandle>();

    private final InternalWorkingMemory workingMemory;
    private final TypeResolver resolver;

    /**
     * This constructor is normally used by Guvnor for running tests on a users request.
     *
     * @param scenario The scenario to run.
     * @param resolver A populated type resolved to be used to resolve the types in
     *                 the scenario.
     *                 <p/>
     *                 For info on how to invoke this, see
     *                 ContentPackageAssemblerTest.testPackageWithRuleflow in guvnor-webapp This
     *                 requires that the classloader for the thread context be set
     *                 appropriately. The PackageBuilder can provide a suitable TypeResolver for
     *                 a given package header, and the Package config can provide a classloader.
     */
    public ScenarioRunner(final Scenario scenario,
                          final TypeResolver resolver,
                          final InternalWorkingMemory wm) throws ClassNotFoundException {
        this.scenario = scenario;
        this.workingMemory = wm;
        this.resolver = resolver;
        runScenario();
    }

    /**
     * Use this constructor if you have a scenario in a file, for instance.
     *
     * @throws ClassNotFoundException
     */
    public ScenarioRunner(String xml,
                          RuleBase ruleBase) throws ClassNotFoundException {
        this.scenario = ScenarioXMLPersistence.getInstance().unmarshal(xml);

        SessionConfiguration sessionConfiguration = new SessionConfiguration();
        sessionConfiguration.setClockType(ClockType.PSEUDO_CLOCK);

        this.workingMemory = (InternalWorkingMemory) ruleBase.newStatefulSession(sessionConfiguration,
                null);


        ClassLoader classLoader = ((InternalRuleBase) ruleBase).getRootClassLoader();
        HashSet<String> imports = getImports(ruleBase.getPackages()[0]);

        this.resolver = new ClassTypeResolver(imports,
                classLoader);
        runScenario();
    }

    public HashSet<String> getImports(Package aPackage) {
        HashSet<String> imports = new HashSet<String>();
        imports.add(aPackage.getName() + ".*");
        imports.addAll(aPackage.getImports().keySet());

        return imports;
    }

    interface Populate {
        public void go();
    }

    private void runScenario() throws ClassNotFoundException {
        MVEL.COMPILER_OPT_ALLOW_NAKED_METH_CALL = true;
        scenario.setLastRunResult(new Date());
        //stub out any rules we don't want to have the consequences firing of.
        HashSet<String> ruleList = new HashSet<String>();
        ruleList.addAll(scenario.getRules());

        TestingEventListener listener = null;

        List<Populate> toPopulate = new ArrayList<Populate>();

        for (final FactData fact : scenario.getGlobals()) {
            final Object factObject = eval("new " + getTypeName(resolver,
                    fact) + "()");
            toPopulate.add(new Populate() {
                public void go() {
                    populateFields(fact,
                            factObject);
                }
            });
            globalData.put(fact.getName(),
                    factObject);
            this.workingMemory.setGlobal(fact.getName(),
                    factObject);
        }

        doPopulate(toPopulate);

        for (Iterator<Fixture> iterator = scenario.getFixtures().iterator(); iterator.hasNext();) {
            Fixture fixture = iterator.next();

            if (fixture instanceof FactData) {
                //deal with facts and globals
                final FactData fact = (FactData) fixture;
                final Object factObject = (fact.isModify()) ? this.populatedData.get(fact.getName()) : eval("new " + getTypeName(resolver,
                        fact) + "()");
                if (fact.isModify()) {
                    if (!this.factHandles.containsKey(fact.getName())) {
                        throw new IllegalArgumentException("Was not a previously inserted fact. [" + fact.getName() + "]");
                    }
                    toPopulate.add(new Populate() {
                        public void go() {
                            populateFields(fact,
                                    factObject);
                            workingMemory.update(factHandles.get(fact.getName()),
                                    factObject);
                        }
                    });
                } else /* a new one */ {
                    populatedData.put(fact.getName(),
                            factObject);
                    toPopulate.add(new Populate() {
                        public void go() {
                            populateFields(fact,
                                    factObject);
                            factHandles.put(fact.getName(),
                                    workingMemory.insert(factObject));
                        }
                    });
                }
            } else if (fixture instanceof RetractFact) {
                RetractFact retractFact = (RetractFact) fixture;
                this.workingMemory.retract(this.factHandles.get(retractFact.getName()));
                this.populatedData.remove(retractFact.getName());
            } else if (fixture instanceof CallMethod) {
                CallMethod aCall = (CallMethod) (fixture);
                Object targetInstance = populatedData.get(aCall.getVariable());
                executeMethodOnObject(aCall,
                        targetInstance);
            } else if (fixture instanceof ActivateRuleFlowGroup) {
                String ruleFlowGroupName = ((ActivateRuleFlowGroup) fixture).getName();
                workingMemory.getAgenda().getRuleFlowGroup(ruleFlowGroupName).setAutoDeactivate(false);
                workingMemory.getAgenda().activateRuleFlowGroup(ruleFlowGroupName);
            } else if (fixture instanceof ExecutionTrace) {
                doPopulate(toPopulate);
                ExecutionTrace executionTrace = (ExecutionTrace) fixture;
                //create the listener to trace rules

                if (listener != null) this.workingMemory.removeEventListener(listener); //remove the old
                listener = new TestingEventListener();

                this.workingMemory.addEventListener(listener);

                //set up the time machine
                applyTimeMachine(this.workingMemory,
                        executionTrace);

                //love you
                long time = System.currentTimeMillis();
                this.workingMemory.fireAllRules(listener.getAgendaFilter(ruleList,
                        scenario.isInclusive()),
                        scenario.getMaxRuleFirings());
                executionTrace.setExecutionTimeResult(System.currentTimeMillis() - time);
                executionTrace.setNumberOfRulesFired(listener.totalFires);
                executionTrace.setRulesFired(listener.getRulesFiredSummary());

            } else if (fixture instanceof Expectation) {
                doPopulate(toPopulate);
                Expectation assertion = (Expectation) fixture;
                if (assertion instanceof VerifyFact) {
                    verify((VerifyFact) assertion);
                } else if (assertion instanceof VerifyRuleFired) {
                    verify((VerifyRuleFired) assertion,
                            (listener.firingCounts != null) ? listener.firingCounts : new HashMap<String, Integer>());
                }
            } else {
                throw new IllegalArgumentException("Not sure what to do with " + fixture);
            }

        }

        doPopulate(toPopulate);
    }

    private void doPopulate(List<Populate> toPopulate) {
        for (Populate p : toPopulate) {
            p.go();
        }
        toPopulate.clear();
    }

    private String getTypeName(TypeResolver resolver,
                               FactData fact) throws ClassNotFoundException {

        String fullName = resolver.getFullTypeName(fact.getType());
        if (fullName.equals("java.util.List") || fullName.equals("java.util.Collection")) {
            return "java.util.ArrayList";
        } else {
            return fullName;
        }
    }

    private void applyTimeMachine(final InternalWorkingMemory wm,
                                  ExecutionTrace executionTrace) {
        long targetTime = 0;
        if (executionTrace.getScenarioSimulatedDate() != null) {
            targetTime = executionTrace.getScenarioSimulatedDate().getTime();
        } else {
            targetTime = new Date().getTime();
        }

        long currentTime = wm.getSessionClock().getCurrentTime();
        ((PseudoClockScheduler) wm.getSessionClock()).advanceTime(targetTime - currentTime,
                TimeUnit.MILLISECONDS);
    }

    void verify(VerifyRuleFired assertion,
                Map<String, Integer> firingCounts) {

        assertion.setActualResult(firingCounts.containsKey(assertion.getRuleName()) ? firingCounts.get(assertion.getRuleName()) : 0);
        if (assertion.getExpectedFire() != null) {
            if (assertion.getExpectedFire()) {
                if (assertion.getActualResult() > 0) {
                    assertion.setSuccessResult(true);
                    assertion.setExplanation("Rule [" + assertion.getRuleName() + "] was actived " + assertion.getActualResult() + " times.");
                } else {
                    assertion.setSuccessResult(false);
                    assertion.setExplanation("Rule [" + assertion.getRuleName() + "] was not activated. Expected it to be activated.");
                }
            } else {
                if (assertion.getActualResult() == 0) {
                    assertion.setSuccessResult(true);
                    assertion.setExplanation("Rule [" + assertion.getRuleName() + "] was not activated.");
                } else {
                    assertion.setSuccessResult(false);
                    assertion.setExplanation("Rule [" + assertion.getRuleName() + "] was activated " + assertion.getActualResult() + " times, but expected none.");
                }
            }
        }

        if (assertion.getExpectedCount() != null) {
            if (assertion.getActualResult().equals(assertion.getExpectedCount())) {
                assertion.setSuccessResult(true);
                assertion.setExplanation("Rule [" + assertion.getRuleName() + "] activated " + assertion.getActualResult() + " times.");
            } else {
                assertion.setSuccessResult(false);
                assertion.setExplanation("Rule [" + assertion.getRuleName() + "] activated " + assertion.getActualResult() + " times. Expected " + assertion.getExpectedCount() + " times.");
            }
        }
    }

    void verify(VerifyFact value) {

        if (!value.anonymous) {
            Object factObject = this.populatedData.get(value.getName());
            if (factObject == null) factObject = this.globalData.get(value.getName());
            FactFieldValueVerifier fieldVerifier = new FactFieldValueVerifier(populatedData,
                    value.getName(),
                    factObject,
                    resolver);
            fieldVerifier.checkFields(value.getFieldValues());
        } else {
            Iterator obs = this.workingMemory.iterateObjects();
            while (obs.hasNext()) {
                Object factObject = obs.next();
                if (factObject.getClass().getSimpleName().equals(value.getName())) {
                    FactFieldValueVerifier fieldVerifier = new FactFieldValueVerifier(populatedData,
                            value.getName(),
                            factObject,
                            resolver);
                    fieldVerifier.checkFields(value.getFieldValues());
                    if (value.wasSuccessful()) return;
                }
            }
            for (VerifyField vfl : value.getFieldValues()) {
                if (vfl.getSuccessResult() == null) {
                    vfl.setSuccessResult(Boolean.FALSE);
                    vfl.setActualResult("No match");
                }
            }
        }
    }

    Object populateFields(FactData fact,
                          Object factObject) {
        for (int i = 0; i < fact.getFieldData().size(); i++) {
            FieldData field = (FieldData) fact.getFieldData().get(i);
            Object val = null;

            if (field.getValue() != null && !field.getValue().equals("")) {

                if (field.getValue().startsWith("=")) {
                    // eval the val into existence
                    val = eval(field.getValue().substring(1),
                            populatedData);
                } else if (field.getNature() == FieldData.TYPE_ENUM) {
                    // The string representation of a java enum value is a
                    // format like CheeseType.CHEDDAR
                    String valueOfEnum = field.getValue();
                    String fullName = null;
                    if (field.getValue().indexOf(".") != -1) {
                        String classNameOfEnum = field.getValue().substring(0,
                                field.getValue().lastIndexOf("."));
                        valueOfEnum = field.getValue().substring(
                                field.getValue().lastIndexOf(".") + 1);
                        try {
                            //This is a Java enum type if the type can be resolved by ClassTypeResolver
                            //Revisit: Better way to determine java enum type or Guvnor enum type.
                            fullName = resolver
                                    .getFullTypeName(classNameOfEnum);
                            if (fullName != null && !"".equals(fullName)) {
                                valueOfEnum = fullName + "." + valueOfEnum;
                            }
                            val = eval(valueOfEnum);
                        } catch (ClassNotFoundException e) {
                            // This is a Guvnor enum type
                            fullName = classNameOfEnum;
                            if (fullName != null && !"".equals(fullName)) {
                                valueOfEnum = fullName + "." + valueOfEnum;
                            }
                            val = valueOfEnum;
                        }
                    } else {
                        val = valueOfEnum;
                    }
                } else {
                    val = field.getValue();
                }

                Map<String, Object> vars = new HashMap<String, Object>();
                vars.putAll(populatedData);
                vars.put("__val__",
                        val);
                vars.put("__fact__",
                        factObject);
                eval("__fact__." + field.getName() + " = __val__",
                        vars);
            }
        }
        return factObject;
    }

    Object executeMethodOnObject(CallMethod fact,
                                 Object factObject) {
        Map<String, Object> vars = new HashMap<String, Object>();
        vars.put("__fact__",
                factObject);
        String methodName = "__fact__." + fact.getMethodName() + "(";
        for (int i = 0; i < fact.getCallFieldValues().length; i++) {
            CallFieldValue field = (CallFieldValue) fact.getCallFieldValues()[i];
            Object val;
            if (field.value != null && !field.value.equals("")) {
                if (field.value.startsWith("=")) {
                    // eval the val into existence
                    val = populatedData.get(field.value.substring(1));
                } else {
                    val = field.value;
                }
                vars.put("__val" + i + "__",
                        val);
                methodName = methodName + "__val" + i + "__";
                if (i < fact.getCallFieldValues().length - 1) {
                    methodName = methodName + ",";
                }

            }
        }
        methodName = methodName + ")";
        eval(methodName,
                vars);
        return factObject;
    }

    /**
     * True if the scenario was run with 100% success.
     */
    public boolean wasSuccess() {
        return this.scenario.wasSuccessful();
    }

    Scenario getScenario() {
        return scenario;
    }

    Map<String, Object> getPopulatedData() {
        return populatedData;
    }

    Map<String, Object> getGlobalData() {
        return globalData;
    }

    Map<String, FactHandle> getFactHandles() {
        return factHandles;
    }

    InternalWorkingMemory getWorkingMemory() {
        return workingMemory;
    }

    TypeResolver getResolver() {
        return resolver;
    }

}
