package com.fujitsu.iflowqa.testframe;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.Date;
import java.util.Hashtable;

import com.fujitsu.iflow.model.util.IflowEnumeration;
import com.fujitsu.iflow.model.util.ModelException;
import com.fujitsu.iflow.model.wfcore.ListSpec;
import com.fujitsu.iflow.model.workflow.DataItemRef;
import com.fujitsu.iflow.model.workflow.Filter;
import com.fujitsu.iflow.model.workflow.Node;
import com.fujitsu.iflow.model.workflow.NodeInstance;
import com.fujitsu.iflow.model.workflow.Plan;
import com.fujitsu.iflow.model.workflow.ProcessInstance;
import com.fujitsu.iflow.model.workflow.TimerDef;
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.WFSession;
import com.fujitsu.iflow.model.workflow.WorkItem;

/**
 * @author Administrator
 *
 * To change the template for this generated type comment go to
 * Window&gt;Preferences&gt;Java&gt;Code Generation&gt;Code and Comments
 */
public class DueDateApiTest extends BPMTestBase {

  private static final long   TIMER_RELATIVE_VALUE = 10000 ;

  private static int PROC_HIST_FIELDS[] = {
                                          ProcessInstance.HISTORY_ID,
                                          ProcessInstance.HISTORY_TIME_STAMP,
                                          ProcessInstance.HISTORY_EVENT_TYPE,
                                          ProcessInstance.HISTORY_EVENTDATA,
                                          ProcessInstance.HISTORY_PRODUCER_TYPE,
                                          ProcessInstance.HISTORY_PRODUCER_ID,
                                          ProcessInstance.HISTORY_CONSUMER_TYPE,
                                          ProcessInstance.HISTORY_CONSUMER_ID,
                                          };

  private TestRecorder     tr = null ;

  public DueDateApiTest() {
  }

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

        try {
            init(tr);
            login();

            //the user must be int he 'Role' group
            assertUserInGroup(username,"Role");

            long[] ids = importTemplates(tr.getTestDir()+"DueDates/",
                    new String[] {"BasicDueDatesAITSV1.xpdl", "VotingDueDatesAITSV1.xpdl"} ) ;

            Plan simpleDueDatePlan = runBasicTests(ids[0]) ;

            importExportTest(simpleDueDatePlan) ;

            Plan votingLoopBackPlan = runBasicTests(ids[1]) ;

            testReActivation(votingLoopBackPlan) ;

            testSuspendResume(simpleDueDatePlan) ;

        }
        catch (Exception e) {
            throw new ExceptionWrapper("TriggerTestCases caught exception in main loop", e) ;
        }
        finally {
            super.close() ;
        }
    }

  public WFSession getSession() {
    return wfSession ;
  }

  public TimerDef addDueDateTimer(Plan pl, long actDefId, long timerDuration) throws ModelException {

    pl.startEdit() ;
    Node n = pl.getNode(actDefId) ;
       // remove any other timers
    TimerDef[] existingTimers = n.getTimers() ;
    for (int i = 0 ; existingTimers != null && i < existingTimers.length; i++)
      n.removeTimer(existingTimers[i]) ;

    String timerUdaName = "DueDateTestTimerValue";
    DataItemRef timerValUda = pl.getDataItemRef(timerUdaName) ;
    if (timerValUda == null) {
      pl.addDataItemRef(timerUdaName, DataItemRef.TYPE_STRING, ""+timerDuration) ;
    } else {
      timerValUda.setValue(""+timerDuration) ;
    }

       // add a due date timer
    TimerDef td = WFObjectFactory.getTimerDef(TimerDef.RELATIVE) ;
    td.setDescription("Testing a Due Date Timer") ;
    td.setName("Due Date Test") ;
    td.setType(TimerDef.RELATIVE) ;
    td.setTime(timerUdaName) ;

    TimerDef realTd = n.setDueDateDef(td) ;

    pl.commitEdit() ;
    tr.markPassed("Added Due date to Activity Node:"+n.getNodeId()) ;
    return realTd ;
  }
  private void checkDueDateExistsOnNode(Plan pl, long actDefId, TimerDef compareTo, String context) throws ModelException {
      // Now read it back
    Node n = pl.getNode(actDefId) ;

    TimerDef readBkDd = n.getDueDateDef();
    TimerDef[] tmrs = n.getTimers() ;

    if (readBkDd == null || tmrs.length > 0) {
      tr.markFailed(context, "Did not find Due Date TimerDef on activity node"+actDefId) ;
      return ;
    }
      // TODO
      // there is no comparator or equals() implementation on the TimerDef so compare here
      // as best we can.
    if (!readBkDd.getDescription().equals(compareTo.getDescription()) ||
        !readBkDd.getName().equals(compareTo.getName()) ||
        !readBkDd.getTime().equals(compareTo.getTime()) ||
        readBkDd.getType() != compareTo.getType()) {
      tr.markFailed(context, "Due Date TimerDef found does not match expected value.") ;
      return ;
    }

    tr.markPassed(context);
  }

    private Plan runBasicTests(long planId) throws ModelException, IOException, Exception
    {

        debug("runBasicTests()") ;
           // get the first activity node in the template
        Plan pl = WFObjectFactory.getPlan(planId, wfSession) ;
        long actId = findFirstActNode(pl) ;
        TimerDef dueDateTimer = addDueDateTimer(pl, actId, TIMER_RELATIVE_VALUE) ;
        checkDueDateExistsOnNode(pl, actId, dueDateTimer, "Basic Store & retrieval of due date");

        ProcessInstance pi = createProcess(pl);
        waitForWorkItem("DueDate 1", wfSession.getUserName(), Filter.MyActiveWorkItems, null, pi.getId(), tr);

        checkTimerStats(pi.getId(), actId, dueDateTimer, TIMER_RELATIVE_VALUE) ;

      // Now sleep for longer than the timer duration and check the status
        debug("Now waiting for longer than timer duration.   This (improper) waiting should be replaced with a SafeSleep wait.");
        try {
          Thread.currentThread().sleep(TIMER_RELATIVE_VALUE+1000) ;
        } catch (InterruptedException intex) {
          throw new ExceptionWrapper("waiting for timer to time out was interrupted?", intex) ;
        }
        long expectedDueDateTime = checkTimerStats(pi.getId(), actId, dueDateTimer, TIMER_RELATIVE_VALUE) ;
        Calendar cal = Calendar.getInstance();
        Date dt = new Date();
        cal.set(2004,Calendar.APRIL,5,9,0);
        Date after = cal.getTime();
        cal.set(2004,Calendar.APRIL,5,10,0);
        Date before = cal.getTime();
        // find it by searching after April 5th
        verifyWorkItemByDueDate(null, after, pi.getId(), expectedDueDateTime, true) ;

        // find it again within a 6 second time window
        verifyWorkItemByDueDate(new Date(expectedDueDateTime + 5000),
                          new Date(expectedDueDateTime - 5000), pi.getId(), expectedDueDateTime, true) ;
        // get rid of the process so that this can be run again.
        pi.abort() ;
        return pl ;
    }

   /**
    * Check the Due Date on an activity instance to ensure it is set properly
    *   and return the due date expiration time.
    *    THIS METHOD ASSUMES the Due Date definition is a RELATIVE timer calculation
    * @param processId - process instance id
    * @param actDefId - activity definition id for the node that should have the due date
    * @param dueDateTimer - timer definition for the due date
    * @param expectedTimerDuration - the duration of the relative timer for the due date (such as 2 days, 2 minutes, etc).
    *
    * @return - time in millis when the due date expires
    * @throws ModelException
    */
  public long checkTimerStats(long processId, long actDefId, TimerDef dueDateTimer,
                              long expectedTimerDuration) throws ModelException {

    long expectedTime = -1 ;

    ProcessInstance pi = WFObjectFactory.getProcessInstance(processId, wfSession) ;
    NodeInstance[] nodes = pi.getMemberNodeInstances() ;
    for (int i = 0 ; i < nodes.length ; i++) {
      if (nodes[i].getNodeId() == actDefId) {

        if (nodes[i].getType() == NodeInstance.TYPE_ACTIVITY ||
            nodes[i].getType() == NodeInstance.TYPE_VOTING_ACTIVITY) {

          Date dd  ;
          NodeInstance ni = (NodeInstance) nodes[i] ;
          dd = ni.getDueDate() ;

          if (dd == null) {
             long ddDefId = -1 ;
             if (dueDateTimer != null)
               ddDefId = dueDateTimer.getId();
             tr.log("Could not retrieve Due Date on Activity Instance (pi, actDefId, dueDateTimerId):("
                 +processId+", "+actDefId+", "+ddDefId) ;
             tr.markFailed("Could not retrieve Due Date on Activity Instance", ""+actDefId);
           } else {
               // verify that the calculated due date is relatively close to what
               // we expect
             expectedTime = getLastActivationTime(pi, ni) + expectedTimerDuration ;
             long delta = dd.getTime() - expectedTime ;
             if (Math.abs(delta) < 2500) // within 2.5 seconds of expected.
               tr.markPassed("Validated Due Date Status on Activity Instance.");
             else
               tr.markFailed("Retrieved Due Date time was not within expected range",
                     "Expected:"+expectedTime + " Due Date Expiration is:"+dd.getTime());
          }
        }
      }
    }
    return expectedTime ;
  }

  private WorkItem verifyWorkItemByDueDate(Date beforeDate, Date afterDate,
                                       long expectedProcId, long dueDateTime,
                                       boolean markPassFail) throws ModelException {

    WFObjectList lister = WFObjectFactory.getWFObjectList(wfSession) ;
    try {
      lister.addFilter(WFObjectList.LISTFIELD_WORKITEM_DUEDATE, WFObjectList.SQLOP_GREATERTHANOREQUALTO,
                        "'"+ afterDate.getTime() +"'") ;
      if (beforeDate != null)
        lister.addFilter(WFObjectList.LISTFIELD_WORKITEM_DUEDATE, WFObjectList.SQLOP_LESSTHAN,
                              "'"+ beforeDate.getTime() +"'") ;
      lister.openBatchedList(Filter.MyActiveWorkItems) ;
      Object[] wis;
      while ((wis = lister.getNextBatch(20)) != null) {
        for (int i = 0 ; i < wis.length ; i++) {
          WorkItem wi = (WorkItem) wis[i] ;
          if (wi.getProcessInstanceId() == expectedProcId) {
            Date dueDate = wi.getDueDate() ;
            long delta = dueDate.getTime() - dueDateTime ;
            if (Math.abs(delta) < 2500) {
              if (markPassFail)
                tr.markPassed("Successfully found WorkItem by filter on Due Date") ;
              return wi ;
            }
          }
        }
      }
    } catch (Throwable t) {
      if (t instanceof ModelException)
        throw (ModelException) t ;

      throw new ModelException(t, 999, "Exception in verifyWorkItemByDueDate()") ;
    } finally {
      lister.closeList() ;
    }
    if (markPassFail)
      tr.markFailed("Did NOT find WorkItem by filter on Due Date", ""+expectedProcId) ;
    return null ;
  }

    /**
     * This test case verifies that Due Dates get reset when a node has been:
     *       1) Activited & Due Date set
     *       2) Then Completed
     *       3) Then later, we return back and re-activate the node.
     * The due date should be reset.
     * @param pl
     * @throws ModelException - should there be a problem with manipulating the process
     * @throws Exception - if a Thread.sleep() is interupted.
     */
  private void testReActivation(Plan pl) throws ModelException, Exception {
      // First take the template and add a due date to the first node
    TimerDef ddTimerDef = addDueDateTimer(pl, findFirstActNode(pl), TIMER_RELATIVE_VALUE) ;

       // run a process and check the due date
    ProcessInstance pi = WFObjectFactory.createProcessInstance(pl.getId(), wfSession) ;
    pi.start() ;
       // wait for first node activation - so sleep a few seconds...
    try {
      Thread.currentThread().sleep(5000) ;
    } catch (InterruptedException intex) {
      throw new ExceptionWrapper("waiting for timer to time out was interrupted?", intex) ;
    }
    long firstDueDateTime = checkTimerStats(pi.getId(), findFirstActNode(pl), ddTimerDef, TIMER_RELATIVE_VALUE) ;

      // get the active workitem and choose "Done"
    WorkItem wi = getActiveWorkItem(pi) ;
    wiChoose(wi, "Done") ;

    long waitTimeOnNode = 7000 ;
      // wait a while on the second node. then choose loop back
    try {
      Thread.currentThread().sleep(waitTimeOnNode) ;
    } catch (InterruptedException intex) {
      throw new ExceptionWrapper("waiting for timer to time out was interrupted?", intex) ;
    }
    // get the active workitem and choose "Loop Back"
    wi = getActiveWorkItem(pi) ;
    wiChoose(wi, "Loop Back") ;
      // wait for first node to activate - so sleep a few seconds...
    try {
      Thread.currentThread().sleep(5000) ;
    } catch (InterruptedException intex) {
      throw new ExceptionWrapper("waiting for timer to time out was interrupted?", intex) ;
    }
    long reActDueDateTime = checkTimerStats(pi.getId(), findFirstActNode(pl), ddTimerDef, TIMER_RELATIVE_VALUE) ;

    if (reActDueDateTime > firstDueDateTime + waitTimeOnNode)
      tr.markPassed("Re-activating node instance with Due Date - resets the Due Date") ;
    else
      tr.markFailed("Re-activating node instance with Due Date DID NOT reset the Due Date",
                    "re-act due date time = "+new Date(reActDueDateTime)+
                    "original Due date was = "+new Date(firstDueDateTime)) ;
  }

    /**
     * Test both Proprietary & XPDL import/export.
     *
     * This test takes a plan - adds a due date to the first node and then exports it
     * It then re-imports that xml and verifies the due date on the first node.
     * @param pl
     * @throws ModelException
     * @throws IOException
     */
  private void importExportTest(Plan pl) throws ModelException, IOException {
      // First take the template and add a due date to the first node
    TimerDef ddTimerDef = addDueDateTimer(pl, findFirstActNode(pl), TIMER_RELATIVE_VALUE) ;
    WFAdminSession adminSess = getAdminSess() ;
      // export that template in proprietary XML
    String propXml = adminSess.savePlanToXML(pl.getId()) ;

      // Now import that template back in.
    Plan importedPl = adminSess.createPlanFromXML(propXml) ;
     // check that the first node still has a due date
    checkDueDateExistsOnNode(pl, findFirstActNode(pl), ddTimerDef, "Looking for Due Date on Proprietary Import/Export.") ;
      // delete the newly imported template
    adminSess.deletePlan(importedPl.getId()) ;

      // Now do the same with XPDL - export it and then import it back in
    File f = File.createTempFile("Aits", "xpdl") ;
    OutputStream outStrm = new FileOutputStream(f) ;
    pl.convertToXPDL(outStrm) ;
    outStrm.close() ;

    Plan newplan = WFObjectFactory.getPlan();
    newplan.setWFSession(wfSession);
    FileInputStream fis = new  FileInputStream(f);
    newplan.convertFromXPDL(fis);
    Plan what = newplan.createProcessDef();
    checkDueDateExistsOnNode(what, findFirstActNode(what), ddTimerDef, "Looking for Due Date on XPDL Import/Export.");
  }


  private void testSuspendResume(Plan pl) throws ModelException, Exception {
      // run a process and check the due date
    ProcessInstance pi = WFObjectFactory.createProcessInstance(pl.getId(), wfSession) ;
    pi.start() ;

      // wait for first node activation - so sleep a few seconds...
    try {
      Thread.currentThread().sleep(5000) ;
    } catch (InterruptedException intex) {
      throw new ExceptionWrapper("waiting for timer to time out was interrupted?", intex) ;
    }
    long expectedDueDateTime = checkTimerStats(pi.getId(), findFirstActNode(pl),
                                               null, TIMER_RELATIVE_VALUE) ;

         // Verify we can find the workitem by Due Date
    WorkItem wi = verifyWorkItemByDueDate(new Date(expectedDueDateTime + 5000),
                            new Date(expectedDueDateTime - 5000), pi.getId(), expectedDueDateTime, true) ;

    pi.suspend() ;
    try {
      Thread.currentThread().sleep(5000) ;
    } catch (InterruptedException intex) {
      throw new ExceptionWrapper("waiting for timer to time out was interrupted?", intex) ;
    }
       // should not be able to find the due date on the suspended process!!!
    wi = verifyWorkItemByDueDate(new Date(expectedDueDateTime + 5000),
                            new Date(expectedDueDateTime - 5000), pi.getId(), expectedDueDateTime, false) ;
    if (wi != null)
      tr.markFailed("Found Work Item by Due Date on SUSPENDED PROCESS!", "found work item id:"+wi.getId());
    else
      tr.markPassed("Checking Due Dates on suspended process.") ;
       // wait a few seconds and resume the process!
    try {
      Thread.currentThread().sleep(5000) ;
    } catch (InterruptedException intex) {
      throw new ExceptionWrapper("waiting for timer to time out was interrupted?", intex) ;
    }
    pi.resume() ;
    try {
      Thread.currentThread().sleep(5000) ;
    } catch (InterruptedException intex) {
      throw new ExceptionWrapper("waiting for timer to time out was interrupted?", intex) ;
    }
       // Should be able to find the workitem again
    wi = verifyWorkItemByDueDate(new Date(expectedDueDateTime + 5000),
                            new Date(expectedDueDateTime - 5000), pi.getId(), expectedDueDateTime, true) ;


  }
    // Just an internal wrapper. Currently AITS requires the only wfsess used to be
    //   a WFAdminSession. But I imagine that restriction will go away and this
    // method can get some other admin session.
  private WFAdminSession getAdminSess() throws ModelException {
    return (WFAdminSession) wfSession ;
  }
  private long findFirstActNode(Plan pl) {
    Node firstNode = null ;
    Node[] nds = pl.getNodes() ;
    for (int i = 0 ; i < nds.length; i++) {
      if (nds[i].getType() == Node.TYPE_ACTIVITY ||
          nds[i].getType() == Node.TYPE_VOTING_ACTIVITY) {
        if (firstNode == null)
          firstNode = nds[i] ;
        else {
          if (firstNode.getPosition().y > nds[i].getPosition().y)
            firstNode = nds[i] ;
        }
      }
    }
    return firstNode.getNodeId() ;
  }

    private void wiChoose(WorkItem wi, String makeChoice) throws ModelException
    {

        String[] choices = wi.getChoices() ;
        for (int i = 0 ; i < choices.length ; i++) {
            if (choices[i].equalsIgnoreCase(makeChoice)) {
                wi.makeChoice(choices[i]) ;
                tr.markPassed("wiChoose");
                return;
            }
        }
        tr.markFailed("wiChoose",  "Did not find Choice named:"+makeChoice+" on work item id="+wi.getId());
    }

    private WorkItem getActiveWorkItem(ProcessInstance pi)
        throws ModelException
    {

        WFObjectList lister = WFObjectFactory.getWFObjectList(wfSession);
        lister.addFilter(WFObjectList.LISTFIELD_PROCESSINSTANCE_ID, WFObjectList.SQLOP_EQUALTO,
                        "'"+ pi.getId() +"'");
        lister.openBatchedList(Filter.MyActiveWorkItems);
        // just return the first work item for me
        Object[] wis = lister.getNextBatch(2);
        lister.closeList() ;
        if (wis != null  && wis.length > 0) {
            return (WorkItem) wis[0];
        }
        throw new ModelException(0, "Did not find an Active WorkItem for this user on that process."+pi.getId());
    }

  private long getLastActivationTime(ProcessInstance pi, NodeInstance ni) throws ModelException {

    long result = -1 ;
    for (IflowEnumeration sElements = pi.getHistory(PROC_HIST_FIELDS);
         sElements.hasMoreElements() ; ) {
      Hashtable histEnt = (Hashtable) sElements.nextElement() ;

      String evtType = (String) histEnt.get(new Integer(ListSpec.PROCESSHISTORY_EVENTTYPE));
      long consumerType = ((Long)histEnt.get(new Integer(ListSpec.PROCESSHISTORY_CONSUMERTYPE))).longValue() ;
      long producerType = ((Long)histEnt.get(new Integer(ListSpec.PROCESSHISTORY_PRODUCERTYPE))).longValue() ;
      long activityInstId = ((Long) histEnt.get(new Integer(ListSpec.PROCESSHISTORY_CONSUMERID))).longValue() ;

      if (consumerType == 3 && producerType == 0 &&  activityInstId == ni.getNodeInstanceId()) {
        Long timeRepresentationInLong = (Long) histEnt.get(new Integer(ListSpec.PROCESSHISTORY_TIMESTAMP));
        result = timeRepresentationInLong.longValue() ;
      }
    }
    return result ;
  }

  /**
  * TestDriver is the class that knows how to run these tests.
  * So just call it with the name of this class in there.
  */
  public
  static
  void
  main(String[] args) {
      String[] newArgs = new String[1];
      newArgs[0] = "com.fujitsu.iflowqa.testframe.DueDateAPITest";
      TestDriver.main(newArgs);
  }

}
