/********************************************************************************
 *                                                                              *
 *  COPYRIGHT (C) 1997-2002 FUJITSU SOFTWARE CORPORATION.  ALL RIGHTS RESERVED. *
 *                                                                              *
 ********************************************************************************/
/*
 * TriggerTestCases.java
 *
 * Created on Dec 5, 2003
 *
 * @author  teisemann
 *
 */
package com.fujitsu.iflowqa.testframe;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Date;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.traversal.NodeIterator;
import org.xml.sax.InputSource;

import com.fujitsu.iflow.common.DataSourceConfParser;
import com.fujitsu.iflow.model.util.ModelException;
import com.fujitsu.iflow.model.wfadapter.DMSObject;
import com.fujitsu.iflow.model.wfadapter.DocRepository;
import com.fujitsu.iflow.model.wfadapter.Folder;
import com.fujitsu.iflow.model.workflow.DMS;
import com.fujitsu.iflow.model.workflow.Filter;
import com.fujitsu.iflow.model.workflow.Plan;
import com.fujitsu.iflow.model.workflow.ProcessInstance;
import com.fujitsu.iflow.model.workflow.TriggerDef;
import com.fujitsu.iflow.model.workflow.TriggerResult;
import com.fujitsu.iflow.model.workflow.WFAdminSession;
import com.fujitsu.iflow.model.workflow.WFObjectFactory;
import com.fujitsu.iflow.model.workflow.WFObjectList;
import com.fujitsu.iflow.model.workflow.TriggerResult.TriggerChoiceMade;

/**
 *  NOTE: this is supposed to be a test of the trigger capability.
 * It is not a good test because it is designed to be very flexible,
 * it is so flexible that it is hard for anything to fail!
 * For example, this test never determines if a trigger first correctly,
 * all it does it check whether any exceptions were thrown.
 * If non thrown, it assumes it all woeked correctly.

 * a better test would have a specific process, with a specific event
 * would test that exactly the right processes are started.
 *
 *
 */
public class TriggerTestCases extends BPMTestBase
{

    private static final String ADMIN_LOGIN_PROP_KEY   =  "adminusername";
    private static final String ADMIN_PWD_PROP_KEY     =  "adminpassword";


    private static final String SIMPLE_START_PROC_XML  = "shiporder1.xml";
    private static final String DO_NOT_START_PROC_XML  = "shiporder2.xml";
    private static final String INVALID_XML            = "invalid.xml";
    private static final String EMP_INSERT_PROC_XML    = "employee_insert1.xml";
    private static final String EMP_UPDATE_PROC_XML    = "employee_update1.xml";

    private static final String WORKING_TRIG_DEF_FILE  = "TriggerEventDef.xml";
    private static final String NON_WORKING_HOST_TRIG_DEF_FILE = "TriggerEventDef2.xml";

    private static final String TRIG_EVT_CONFIG_FILE_NAME       = "TriggerEventDef.xml";
    private static final String TRIG_DEF_ORIG_FILE_NAME         = "TriggerEventDef.xml.orig";
    private static final int    LITTLE_MORE_THAN_A_MINUTE_MS    = (65 * 1000);

    private DocRepository     docrepos;
    private String            dmsConfDirPath;

    private List              importedTemplateIds;

    public TriggerTestCases() {
        importedTemplateIds = new ArrayList();
    }

    /* (non-Javadoc)
    * @see com.fujitsu.iflowqa.testframe.TestSet#runTests(com.fujitsu.iflowqa.testframe.TestRecorder)
    */
    public void runTests(TestRecorder ntr)
    throws Exception
    {
        init(ntr);

        // login to iFlow and import appropriate templates
        // plus any other initialization
        initQ();


        try {
            /* Some of the templates loaded contain SSA trigger definitions. Those triggers will
             * cause FileNotFoundExceptions when their java Script trigger definitions are evaluated
             * IF THERE is no TriggerEventDef.xml file present. So first check to see if the Trigger EventDef file
             * is present. If not then just copy one there as a place holder.
             *
             * IF the file IS THERE - BACK IT UP AND WE WILL RESTORE IT LATER!
             */
            if (doesFileExistInConfigDir(TRIG_EVT_CONFIG_FILE_NAME)) {
                tr.log("Backing up original TriggerEventDef.xml to "+TRIG_DEF_ORIG_FILE_NAME);
                copyConfigFile(TRIG_EVT_CONFIG_FILE_NAME, TRIG_DEF_ORIG_FILE_NAME);
            }
            else {
                copyTriggerEventFile(WORKING_TRIG_DEF_FILE);
            }

            try {

            testStartProcessTriggers();
            testSsaDbTriggers();
            testXmlObjCache();

            }
            finally {
                cleanUpTriggerEventDefFile();
            }
        }
        finally {
            cleanUpAndClose();
        }
    }

    protected void initQ()
    throws Exception
    {
        iFlowLogin();

        StringTokenizer st = new StringTokenizer(wfSession.getServerOption("DMSRoot"), ",");
        StringBuffer srvrPath = new StringBuffer();
        while (st.hasMoreTokens()) {
            String one = st.nextToken();
            if (one.indexOf("FormsRepository") != -1) {
                srvrPath.append(one);
                break;
            }
        }
        srvrPath.append("../");
        srvrPath.append(DataSourceConfParser.DMS_CONFIG_FILE_DIR).append(DMS._pathSeparator);

        dmsConfDirPath = srvrPath.toString();
        // to get this to work on Solaris & other unix variants, we need to convert this
        // path to Microsoft format.. This is bizaire as it kind of forces client/api code to
        // know the platform of the server..
        dmsConfDirPath = dmsConfDirPath.replace('/', '\\');

        String s = tr.getProperty("load_xpdls", "Aits_ssa_trigs_testV1.xpdl,Aits_Trigs_simple_xsdV1.xpdl");
        debug("import templates="+s);
        if (s != null) {
            st = new StringTokenizer(s, ",");

            debug("import N templates="+st.countTokens());
            String[] names = new String[st.countTokens()];
            for (int i = 0; st.hasMoreTokens(); i++) {
                names[i] = st.nextToken();
                debug("template name="+names[i]);
            }
            importTemplates(names, tr);
        }
    }

    private void testStartProcessTriggers()
    throws Exception
    {
        // positive test - should start a process
        String testCtxt = "Simple Start Process Trigger";
        String eventXml = loadXmlFile(SIMPLE_START_PROC_XML);
        TriggerResult rslt = runEventXml(eventXml);
        if (!rslt.succeeded()) {
            handleTrigTestFailure(rslt, tr, testCtxt, eventXml);
        }

        long[] startedProcIds = rslt.getStartedProcessInstanceIds();
        if (startedProcIds.length < 1) {
            tr.markFailed(testCtxt, "Event did not start a process.");
        }
        else {
            tr.markPassed(testCtxt +" started piid="+startedProcIds[0]);
        }

        // now check the data mapping
        testCtxt = "Test Data Mapping on Simple Start Process Trigger";
        if (startedProcIds.length > 0) {
            wasDataMappingSuccessful(startedProcIds, eventXml);
        }
        // negative test - should not start a process because of the Event Filter
        testCtxt = "Event Filter Not Start Process Trigger";
        eventXml = loadXmlFile(DO_NOT_START_PROC_XML);
        rslt = runEventXml(eventXml);
        if (!rslt.succeeded()) {
            handleTrigTestFailure(rslt, tr, testCtxt, eventXml);
        }

        startedProcIds = rslt.getStartedProcessInstanceIds();
        if (startedProcIds.length > 0) {
            tr.markFailed(testCtxt, "Event SHOULD NOT HAVE started a process.");
        }
        else {
            tr.markPassed(testCtxt);
        }

        // negative test - pass invalid XML should cause error
        testCtxt = "Invalid XML causes error";
        try {
            String badExample = loadXmlFile(INVALID_XML);
            rslt = runEventXml(badExample);
            if (rslt.succeeded()) {
                handleTrigTestFailure(rslt, tr, testCtxt, badExample);
                tr.markFailed(testCtxt, "Event SHOULD have caused errors in the Trigger Handler, but didn't.");
            }
            else {
                tr.markPassed(testCtxt);
            }
        }
        catch(Exception ee){
            tr.markPassed(testCtxt+" - got expected exception :"+ee.toString());
        }
    }

    private void testSsaDbTriggers()
    throws Exception
    {
        // Need to get the TriggerEventDef.xml to the right install directory
        copyTriggerEventFile(WORKING_TRIG_DEF_FILE);

        // The TriggerEventDef.xml file gets read and "cached" by one of the scriptable objects
        // inside the TriggerHandler. That cache will only look at the File Modification time every
        // 60 seconds. So, since we changed the TriggerEventDef.xml file - let's sleep
        // for a minute to ensure our changes get caught.
        tr.log("Waiting 60 seconds for TriggerHandler to read TriggerEventDef.xml");
        Thread.currentThread().sleep(LITTLE_MORE_THAN_A_MINUTE_MS);

        // positive test - should start a process
        String testCtxt = "Employee Insert Start Process Trigger";
        String eventXml = loadXmlFile(EMP_INSERT_PROC_XML);
        TriggerResult rslt = runEventXml(eventXml);
        if (!rslt.succeeded()) {
            handleTrigTestFailure(rslt, tr, testCtxt, eventXml);
        }

        long[] startedProcIds = rslt.getStartedProcessInstanceIds();
        if (startedProcIds.length < 1) {
            tr.markFailed(testCtxt, "Event did not start a process.");
        }
        else {
            tr.markPassed(testCtxt +" started piid="+startedProcIds[0]);
        }

        // now check the data mapping
        testCtxt = "Test Data Mapping on Simple Start Process Trigger";
        if (startedProcIds.length > 0) {
            wasDataMappingSuccessful(startedProcIds, eventXml);
        }

        //After starting a process. In slow machine it may take some time
        //for workitem to appear, because this  depend on Enactment opf the
        //Activity active event.
        tr.log("Waiting 60 seconds for WorkItem to appear before make choice.");
        Thread.currentThread().sleep(LITTLE_MORE_THAN_A_MINUTE_MS);

        // positive test - should make a
        testCtxt = "Employee Update Start Process Trigger";
        String anotherExample = loadXmlFile(EMP_UPDATE_PROC_XML);
        rslt = runEventXml(anotherExample);
        if (!rslt.succeeded())
            handleTrigTestFailure(rslt, tr, testCtxt, anotherExample);

        TriggerChoiceMade[] choices = rslt.getChoicesMade();
        if (choices == null || choices.length < 1)
            tr.markFailed(testCtxt, "Event did not make a choice.");
        else
            tr.markPassed(testCtxt +" made N choices="+choices.length);
    }

    private void testXmlObjCache()
    throws Exception
    {
        // Need to copy another TriggerEventDef.xml to the right install directory
        // this one has a different <host> that will not match anything.
        // this tests that the XmlObjCache catch the file mod time change!
        copyTriggerEventFile(NON_WORKING_HOST_TRIG_DEF_FILE);

        // The TriggerEventDef.xml file gets read and "cached" by one of the scriptable objects
        // inside the TriggerHandler. That cache will only look at the File Modification time every
        // 60 seconds. So, since we changed the TriggerEventDef.xml file - let's sleep
        // for a minute to ensure our changes get caught.
        tr.log("Waiting 60 seconds for TriggerHandler to read TriggerEventDef.xml");
        Thread.currentThread().sleep(LITTLE_MORE_THAN_A_MINUTE_MS);

        // now pass the same employee insert. It should NOT start a process
        String testCtxt = "Test XmlObjCache check on File modification time.";
        String eventXml = loadXmlFile(EMP_INSERT_PROC_XML);
        TriggerResult rslt = runEventXml(eventXml);
        if (!rslt.succeeded()) {
            handleTrigTestFailure(rslt, tr, testCtxt, eventXml);
        }

        long[] startedProcIds = rslt.getStartedProcessInstanceIds();
        if (startedProcIds.length > 0) {
            tr.markFailed(testCtxt, "Event started a process when it should NOT have."+startedProcIds[0]);
        }
        else {
            tr.markPassed(testCtxt +" Trigger Handler caught file mod time change on TriggerEventDef.xml");
        }
    }

    private void iFlowLogin()
    throws Exception
    {
        super.login();
        DMS dms = WFObjectFactory.getDMS(wfSession, username, password);
        docrepos = WFObjectFactory.getDocRepository(dms);
    }

    private void importTemplates(String[] templateNames, TestRecorder tr)
    throws Exception
    {

        tr.log("importing templates from NORMAL test directory");
        for (int i = 0; i < templateNames.length; i++) {
            debug("importing template="+templateNames[i]);
            Plan p = importXPDL("Triggers/"+templateNames[i]);
            importedTemplateIds.add(new Long(p.getId()));
        }
    }

    /**
     * Clean up
     *
     */
    private void cleanUpAndClose()
    throws Exception
    {
        try {
            super.logOut();

            username = tr.getRequiredProperty(ADMIN_LOGIN_PROP_KEY);
            password = tr.getRequiredProperty(ADMIN_PWD_PROP_KEY);
            iFlowLogin();

            for (Iterator itr = importedTemplateIds.iterator(); itr.hasNext(); )  {
                long planid = ((Long) itr.next()).longValue();
                removeProcsAndPlan(wfSession, planid);
            }
            super.logOut();
        }
        catch (ModelException mex) {
            logError("iFlow Logout failed.", mex);
        }
    }

    private void removeProcsAndPlan(WFAdminSession adminSess, long planId)
    {

        WFObjectList lstr = null;
        try {
            lstr = WFObjectFactory.getWFObjectList(adminSess);

            lstr.addFilter(WFObjectList.LISTFIELD_PLAN_ID, WFObjectList.SQLOP_EQUALTO, "'"+planId+"'");
            lstr.openBatchedList(Filter.AllProcesses);
            for ( Object[] procs = lstr.getNextBatch(10); procs != null; procs = lstr.getNextBatch(10)) {
                for (int i = 0; i < procs.length; i++) {
                    ProcessInstance one = ((ProcessInstance) procs[i]);
                    adminSess.deleteProcessInstance(one.getId());
                }
            }

            adminSess.deletePlan(planId);
        }
        catch (Throwable t) {
        }
        finally {
            if (lstr != null) {
                try {
                    lstr.closeList();
                }
                catch (ModelException giveup) {
                }
            }
        }
    }

    private String  loadXmlFile(String eventFileName)
    throws Exception
    {
        File f = new File(tr.getTestDir()+"Triggers/"+eventFileName);
        if (!f.exists()) {
            throw new FileNotFoundException("Cannot find Trigger event file ->"+f.getCanonicalPath());
        }

        char[] buff = new char[(int) f.length()];
        FileInputStream fis = new FileInputStream(f);
        Reader rdr = new InputStreamReader(fis, "UTF-8");
        rdr.read(buff);
        rdr.close();
        fis.close();

        return new String(buff);
    }

    private TriggerResult runEventXml(String eventXml)
    throws Exception
    {
        return WFObjectFactory.processTriggerEvent(eventXml, wfSession);
    }

    private void handleTrigTestFailure(TriggerResult result, TestRecorder tr,
            String context, String xmlEvent)
            throws Exception
    {
        tr.log("This event XML caused problems:\n"+xmlEvent);
        long[] ids = result.getStartedProcessInstanceIds();
        debug("started N processes:"+ids.length);
        if (ids.length > 0) {
            for (int i = 0; i < ids.length; i++) {
                debug("Started Process with ID:"+ids[i]);
            }
        }
        TriggerChoiceMade[] choices = result.getChoicesMade();
        debug("made N choices:"+choices.length);
        if (choices.length > 0) {
            for (int i = 0; i < choices.length; i++) {
                debug("Made Choice:"+choices[i].getChoiceMade() +"," + choices[i].getActivityInstanceId() +","
                        +choices[i].getProcessInstanceId());
            }
        }

        Exception[] errs = result.getExceptions();
        if (errs != null && errs.length > 0) {
            for (int i = 0; i < errs.length; i++) {
                logError("Trigger Exceptions:",errs[i]);
            }
            throw new ModelException(0, "Trigger test failed:" + context);
        }
        tr.markFailed(context, "Event caused unexpected results.");
    }

    private void wasDataMappingSuccessful(long[] piids, String eventXml)
    throws Exception
    {
        try {
            for (int i = 0; i < piids.length; i++) {
                ProcessInstance pi = WFObjectFactory.getProcessInstance(piids[i], wfSession) ;
                Plan theplan = pi.getPlan();
                TriggerDef[] tds = theplan.getTriggers();
                Hashtable datamap = tds[0].getDataMap();

                DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = docFactory.newDocumentBuilder();
                Document doc = builder.parse(new InputSource(new StringReader(eventXml)));

                for (Iterator itr = datamap.keySet().iterator(); itr.hasNext();) {
                    String udaName = (String) itr.next();
                    testAndReport("Data map for uda "+udaName,
                            getXPath(doc, (String)datamap.get(udaName)),
                            pi.getDataItem(udaName).getValue());
                }
            }
        }
        catch (Exception xmlex) {
            throw new ExceptionWrapper("Exception while testing Data Map", xmlex);
        }
    }

    private String getXPath(Document xml, String xpathExpr)
    throws Exception
    {
        NodeIterator nl = XPathAPI.selectNodeIterator(xml, xpathExpr);
        StringBuffer sb = new StringBuffer();
        Node n;
        while ((n = nl.nextNode())!= null) {
            sb.append(n.getNodeValue());
            // TODO - should siblings be here>>>??
        }
        String val = sb.toString();
        //debug("getXPath("+xpathExpr+") returned "+val);
        return val;
    }

    /**
     * Copy a new TriggerEventDef.xml config file.
     *
     *
     * @param sourceFileName - name of the config file that will be the SOURCE for the copy
     * @param backupExisting - if true - and a TriggerEventDef.xml file exists then it will be backed up.
     * @throws Exception
     */
    private void copyTriggerEventFile(String sourceFileName)
    throws Exception
    {
        File srcFile = new File(tr.getTestDir()+"Triggers/"+sourceFileName);
        copyLocalFileToConfigDir(srcFile, TRIG_EVT_CONFIG_FILE_NAME);
    }
    /**
     * Copy a file.
     *
     * @param srcFileName - filename that will be the SOURCE for the copy in the server config dir
     * @param dstFileName - filename that will be the DESTINATION for the copy in the server config dir
     * @throws Exception
     */
    private void copyConfigFile(String srcFileName, String dstFileName) throws Exception
    {
        // copy the file.
        tr.log(new Date() + ":Copying Config files From: "+srcFileName + " To: "+dstFileName);
        // This is done by retrieving the source file from the DMS to a local temp file
        // then copying it back...

        File tempFile = null;
        try {
            tempFile = File.createTempFile("bckup", ".xml");

            String srcPath = dmsConfDirPath + srcFileName;
            debug( "checking out config file:"+srcPath);
            debug( "to local file:"+tempFile.getCanonicalPath());
            docrepos.checkOut(srcPath, tempFile, false);

            copyLocalFileToConfigDir(tempFile, dstFileName);
        }
        catch (Exception  ex) {
            logError("error copying server config file", ex);
            throw ex;
        }
        finally {
            if (tempFile != null) {
                tempFile.delete();
            }
        }
    }

    public void
    logError(String msg, Exception e)
    throws Exception
    {
        throw new ExceptionWrapper(msg,e);
    }

    private void copyLocalFileToConfigDir(File src, String dstFileName) throws ModelException, IOException
    {
        String srvrPath = dmsConfDirPath + dstFileName;
        docrepos.checkInNew(srvrPath, src.getCanonicalPath(), true);
    }

    private void cleanUpTriggerEventDefFile() throws Exception
    {
        // IF we backed up the original TriggerEventDef.xml file...
        // then restore it
        if (doesFileExistInConfigDir(TRIG_DEF_ORIG_FILE_NAME)) {
            copyConfigFile(TRIG_DEF_ORIG_FILE_NAME, TRIG_EVT_CONFIG_FILE_NAME);
            // TODO - THIS delete throws an exception... The DMS adapter does not allow
            // deleting from the config directory - This could be considered a bug in the
            // dms adapter - or maybe find someother way to handle it.
            //docrepos.deleteDocument(dmsConfDirPath+TRIG_DEF_ORIG_FILE_NAME);
        }
        else {
            // if we did not have to restore the original - well then delete the test case version
            // of the TriggerEventDef.xml. There must not have been one when we started.
            // TODO - THIS delete throws an exception... The DMS adapter does not allow
            // deleting from the config directory - This could be considered a bug in the
            // dms adapter - or maybe find someother way to handle it.
            //docrepos.deleteDocument(dmsConfDirPath+TRIG_EVT_CONFIG_FILE_NAME);
        }
    }

    private boolean doesFileExistInConfigDir(String fileName) throws ModelException
    {
        Folder configDir = docrepos.getFolder(dmsConfDirPath);
        DMSObject[] files = configDir.getContents();
        for (int i = 0; i < files.length; i++) {
            if (files[i].getName().equals(fileName)) {
                return true;
            }
        }
        return false;
    }
}
