/*
** Module   : Test.java
** Abstract : defines and manages the execution of a single test 
**
** Copyright (c) 2009-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090113 Stores the steps for a test and manages the execution of those steps.
** 002 CA  20210827 Refactored to allow other test types, beside terminal (which can be executed from the same 
**                  test suite).  Added SOAP and REST web service tests.
** 003 TJD 20220803 Added logging, updated for Java 11
** 004 TJD 20220803 Changed log format
** 005 TJD 20230612 Support for IN_PROGRESS test status
** 006 TJD 20240201 Benchmark mode corrections
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License
** along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.goldencode.harness.test;

import java.util.*;

import com.goldencode.util.*;
import com.goldencode.harness.*;

/**
 * Stores the steps for a test and manages the execution of those steps.
 */
public class Test
extends Report
implements Testable
{
   /** Descriptive text for the dependency chaining. */
   private static final String CHAIN = "dependency chain:";
   
   /** Maximum milliseconds before failing, even if no other failure occurs. */
   private final long timeout;
   
   /** The steps to execute in the correct order of execution. */
   private List<Testable> steps = null;
   
   /** Cleanup processing to run after testing is complete (even on failure). */
   private List<Cleanup> cleanups = null;
   
   /** Object to notify of completion status. */
   private List<Dependent> dependents = null;
   
   /** The steps to execute in <code>BACKOUT</code> failure mode. */
   private Test backout = null;
   
   /** The driver used to execute this test. */
   private Driver driver = null;
   
   /**
    * Create a new instance with the given steps as the contents of the
    * test.
    *
    * @param    name
    *           Name of the test.
    * @param    descr
    *           Verbose description of the test.
    * @param    steps
    *           The list of steps for execution, in the proper order of
    *           execution.
    * @param    cleanups
    *           The list of cleanup processing to execute after testing (even
    *           on failure).
    * @param    timeout
    *           Maximum milliseconds before failing, even if no other failure
    *           occurs. Use -1 to disable the test-level timeout.
    */
   public Test(String         name,
               String         descr,
               List<Testable> steps,
               List<Cleanup>  cleanups,
               long           timeout)
   {
      super(name, descr);
      this.steps    = steps;
      this.cleanups = cleanups;
      this.timeout  = timeout;
   }
   
   /**
    * Set the {@link #driver}.
    * 
    * @param    driver
    *           The driver.
    */
   public void setDriver(Driver driver)
   {
      this.driver = driver;
   }
   
   /**
    * Set the new status of the testable activity.
    *
    * @param    status
    *           New status for the testable activity.
    */
   public void setStatus(Status status)
   {
      super.setStatus(status);
      
      // notify all dependent objects of the final status
      if (dependents != null && !status.equals(Status.IN_PROGRESS))
      {
         String reason = getReason();
         String name   = getName();
         String descr  = name;
         
         // build a descriptive string for the chain of failed dependencies
         if (status == Status.FAILED_DEPENDENCY)
         {
            // continue the chain
            descr = String.format("%s --> %s", reason, name);
         }
         else
         {
            // start the chain here
            descr = String.format("%s %s", CHAIN, name);
         }
         
         Iterator<Dependent> iter = dependents.iterator();
         
         while (iter.hasNext())
         {
            Dependent dependent = iter.next();
            dependent.update(status, descr);
         }
      }
   }
   
   /**
    * Get the list of steps to run.
    *
    * @return   The list of steps or <code>null</code> if none have been set.
    */
   public List<Testable> getSteps()
   {
      return steps;
   }
   
   /**
    * Set the list of steps to run.
    *
    * @param    steps
    *           The list of steps to run.
    */
   public void setSteps(List<Testable> steps)
   {
      this.steps = steps;
   }
   
   /**
    * Get the list of cleanup processors.
    *
    * @return   The list of cleanup processors or <code>null</code> if none
    *           have been set.
    */
   public List<Cleanup> getCleanups()
   {
      return cleanups;
   }
   
   /**
    * Set the list of cleanup processors.
    *
    * @param    cleanups
    *           The list of cleanup processors or <code>null</code> if none
    *           are to be run.
    */
   public void setCleanups(List<Cleanup> cleanups)
   {
      this.cleanups = cleanups;
   }
   
   /**
    * Get the number of sub-elements of the testable activity.
    *
    * @return   If the testable activity is a single atomic activity, then
    *           this will return 1. Otherwise, it will return the number of
    *           individual sub-elements to be executed in this activity.
    */
   public int getNumberOfElements()
   {
      return steps.size();
   }
   
   /**
    * Add to the list of dependent objects. The given instance will receive a
    * notification upon completion with the details of the status.
    *
    * @param    dependent
    *           The object to notify when complete.
    */
   public void addDependent(Dependent dependent)
   {
      if (dependents == null)
      {
         dependents = new ArrayList<Dependent>();
      }
      
      dependents.add(dependent);
   }
   
   /**
    * Get the backout steps to execute. This supports the <code>BACKOUT</code>
    * failure mode. This method also appends the steps from the backout test
    * onto the step list of this test itself (so that the backout steps can be
    * seen in any report output).
    * <p>
    * Restrictions:
    * <p>
    * <ul>
    *    <li> This should only be called if the script is going to be
    *         executed since it permanently changes the steps list.
    *    <li> This should only be called once (otherwise the steps will be
    *         copied more than once).
    *    <li> All processing of the steps list must already have completed
    *         since the list itself is changing.
    * <ul>
    *
    * @return   The steps to execute to backout a failed test.
    */
   public Test getBackout()
   {
      if (backout != null)
         steps.addAll(backout.steps);
      
      return backout;
   }
   
   /**
    * Set the backout steps to execute. This supports the <code>BACKOUT</code>
    * failure mode.
    *
    * @param    backout
    *           The steps to execute to backout a failed test.
    */
   public void setBackout(Test backout)
   {
      this.backout = backout;
   }
   
   /**
    * Execute the full set of test directives in the proper order, until all
    * directives have successfully completed or until one of the directives
    * fails. This method returns as soon as the first failure occurs, without
    * executing any following directives.
    */
   @Override
   public void execute()
   {
      setThreadName(Thread.currentThread().getName());
      
      Status status = Status.NOT_RUN;
      String reason = null;
      
      // start test level timer
      long start = System.currentTimeMillis();
      setStartTime(start);
      LogHelper.finer("START|"+driver.getName() + "|" + this.getName());
      setStatus(Status.IN_PROGRESS);
      
      try
      {
         // iterate through all steps until a failure occurs or all are run
         // successfully
         for (int i = 0; i < steps.size(); i++)
         {
            Testable next = steps.get(i);
            
            // safety first
            if (next == null)
               continue;
            
            // start step timer
            next.setStartTime(System.currentTimeMillis());
            
            // run the step
            try
            {
               LogHelper.finer("STEPSTART|" + driver.getName() + "|" + this.getName() +  "|" + (i + 1) 
                        +"|Step " + ( i + 1) + ": " + next.getName() + ":" + getDescription());
               long testStartTime = System.currentTimeMillis();
               driver.execute(next);
               long testEndTime = System.currentTimeMillis();
               LogHelper.finer("TIME|" + driver.getName() + "|" + this.getName() + "|" + (i + 1) 
                        +"|" + (testEndTime - testStartTime) + "|" + next.getMaxElapsed()  
                        + "|Step " + ( i + 1) + ": " + next.getName() 
                        + " step time -> " + (testEndTime - testStartTime) 
                        + " ms, max time:" + next.getMaxElapsed() + " ms");
            }
            
            catch (Throwable thr)
            {
               // unexpected failure, update the step's state to ensure that
               // the failure can have a useful description downstream
               
               reason = next.getReason();
               
               String prob = LogHelper.dumpThrowable(thr);
               
               if (reason == null)
               {
                  reason = prob;
               }
               else
               {
                  reason = String.format("%s (%s)", reason, prob);
               }
               
               next.setReason(reason);
               next.setStatus(Status.FAILED);
            }
            
            // end step timer
            next.setEndTime(System.currentTimeMillis());
            
            status = next.getStatus();
            
            // exit on any failure
            if (status != Status.PASSED)
            {
               String stepReason = next.getReason();
               
               if (stepReason == null)
               {
                  stepReason = "";
               }
               else
               {
                  stepReason = String.format(": '%s'", stepReason);
               }
               
               reason = String.format("failure in step %d%s", (i + 1), stepReason); 
               break;
            }
         }
      }
      
      finally
      {
         // process cleanups
         if (cleanups != null)
         {
            Iterator<Cleanup> iter = cleanups.iterator();
            
            while (iter.hasNext())
            {
               Cleanup cleaner = iter.next();
               
               try
               {
                  // best efforts cleanup hook
                  cleaner.execute();
               }
               
               catch (Throwable thr)
               {
                  // ignore this
               }
            }
         }
      }
   
      // end the test timer
      long end = System.currentTimeMillis();
      setEndTime(end);
      long elapsed = end - start;
      LogHelper.finer("END|"+driver.getName() + "|" + this.getName() + "| Ended in (ms) |" + elapsed);
      
      
      if (timeout >= 0 && elapsed > timeout)
      {
         status = Status.FAILED;
         reason = String.format("Timeout after %d milliseconds. Maximum " +
                                "milliseconds of %d was exceeded for the " +
                                "test. No other failures occurred.",
                                elapsed,
                                timeout);
      }
      
      // update test state
      setStatus(status, reason);
      
      calculateStatusCounter(steps);
   }
}
