/*
** Module   : ScreenPoller.java
** Abstract : base class to handle common polling logic when examining screens
**
** Copyright (c) 2009-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090210 Base class to handle common polling logic when examining screens.
** 002 CA  20130509 Added getter for the millis field.
** 003 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.
** 004 TJD 20220816 Log also expected screen in case test fails
** 005 TJD 20220817 implemented performance test mode with experimental remove of waits from screen polling,
** 006 TJD 20220818 Fix for performance mode bug
** 007 TJD 20220819 Allow configuring max timeout for single test that require idle waiting
** 008 TJD 20230719 Test timeout multiplier support, Reduce screen poll delay, increase screen poll frequency
** 009 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 com.goldencode.harness.*;
import com.goldencode.harness.terminal.*;

/**
 * Base class to handle common polling logic when examining screens.
 */
public abstract class ScreenPoller
extends Report
implements TerminalTestable
{
   /** Milliseconds to wait in polling loop. */
   private static final int INTERVAL = 100;
   
   /** Maximum number of milliseconds to wait. */
   private final int millis;
   
   /** The most recent reason that screen comparisons failed. */
   private String lastReason = null;
   
   /** The last not screen before clearing */
   private String expectedScreen = null;
   
   /**
    * Construct an instance which polls for a screen buffer to be available.
    *
    * @param    name
    *           Short name of the testable activity.
    * @param    descr
    *           Verbose description of the testable activity.
    * @param    millis
    *           The maximum number of milliseconds to wait for a screen to
    *           appear. Use -1 to for an indefinite wait (no timeout) and use
    *           0 to check the current screen and immediately return.
    */
   public ScreenPoller(String name, String descr, int millis)
   {
      super(name, descr);
      this.millis = millis;
   }
   
   /**
    * Get the maximum number of milliseconds to wait, from the {@link #millis} field.
    * 
    * @return   See above.
    */
   public int getMillis()
   {
      return millis;
   }
   
   /**
    * ExperimentalPerformance mode version : 
    * Execute the directive of the test using the given terminal as the data
    * sink and/or source for the operation. 
    * Immediate checks (with timeout == 0) return last valid screen, so static screens can be checked several
    * times
    * @param    term
    *           The terminal that represents the system under test.
    */
   public void executePM(Terminal term)
   {
      Status   status = Status.FAILED;
      String   reason = null;
      
      int      amt        = 0;
      int      timeout    = millis;
      boolean  oneTime    = (millis == 0);
      boolean  indefinite = (millis == -1);
      boolean  failure    = false;
      char[][] screen     = null;
      char[][] validScreen     = null;
      
      long waitPeriodStart = System.currentTimeMillis();
      
      while (timeout > 0 || indefinite || oneTime)
      {
         failure = false;
         // immediately get the current screen
         screen = term.getScreen(false, 0);
         if (screen != null)
         {
            reason = tryTest(term, screen);
            
            if (reason != null)
               failure = true;
            
            // if the reason is null, then we have found our match OR if the
            // testable activity is complete when the first screen has been
            // processed
            if (firstScreenOnly() || reason == null)
            {
               break;
            }
         }
         else
         {
            if (oneTime)
               reason = "timeout reached before a screen was available";
         }
         
         if (oneTime)
           break;
         
         // Store screen for optimization
         validScreen = screen;
         // second call to getScreen will block if there were no screen changes
         screen = term.getScreen(false, timeout);
         // Check again if screen has been changed
         if (screen != null && screen != validScreen)
         {
            reason = tryTest(term, screen);
            
            if (reason != null)
               failure = true;
            
            // if the reason is null, then we have found our match OR if the
            // testable activity is complete when the first screen has been
            // processed
            if (firstScreenOnly() || reason == null)
            {
               break;
            }
         }
         
         long waitPeriodEnd = System.currentTimeMillis();
         
         if (!indefinite)
         {
            // calculate remaining timeout value
            timeout -= (waitPeriodEnd-waitPeriodStart);
            waitPeriodStart = waitPeriodEnd;
            
            // check if a timeout has occurred
            if (timeout <= 0)
            {
               failure = true;
               if (firstScreenOnly())
               {
                  reason = "timeout before any screen buffer became available";
               }
               else
               {
                  reason = "timeout before the specific screen buffer became " +
                           "available";
                           
                  if (lastReason != null)
                  {
                     // make a more specific failure reason using specific
                     // screen comparison data
                     reason = String.format("%s (%s)", reason, lastReason);
                  }
               }
            }
         }
      }
      
      if (reason == null)
      {
         status = Status.PASSED;
         
         updateDescription(true, screen != null ? screen : validScreen);
      }
      else
      {
         // on a failure due to screen mis-matching, document the last screen
         if (failure)
         {
            updateDescription(false, screen != null ? screen : validScreen);
         }
      }
      
      setStatus(status, reason);
   }
   
/**
    * Execute the directive of the test using the given terminal as the data
    * sink and/or source for the operation.
    *
    * @param    term
    *           The terminal that represents the system under test.
    */
   @Override
   public void execute(Terminal term)
   {
      if (Harness.isBenchmarkMode())
      {
         executePM(term);
         return;
      }
      Status   status = Status.FAILED;
      String   reason = null;
      
      int      amt        = 0;
      boolean  oneTime    = (millis == 0);
      boolean  indefinite = (millis == -1);
      int      timeout    = Harness.scaleTimeoutMultiplier(millis);
      boolean  failure    = false;
      char[][] screen     = null;
      
      while (timeout > 0 || indefinite || oneTime)
      {
         failure = false;
         
         screen = term.getScreen(false, 0);
         
         if (screen != null)
         {
            reason = tryTest(term, screen);
            
            if (reason != null)
               failure = true;
            
            // if the reason is null, then we have found our match OR if the
            // testable activity is complete when the first screen has been
            // processed
            if (firstScreenOnly() || reason == null)
            {
               break;
            }
         }
         else
         {
            if (oneTime)
               reason = "timeout reached before a screen was available";
         }
         
         if (oneTime)
            break;
         
         try
         {
            amt = indefinite ? INTERVAL : Math.min(timeout, INTERVAL); 
            Thread.sleep(amt);
         }
         
         catch (InterruptedException ie)
         {
            reason = "thread interrupted";
            break;
         }
         
         if (!indefinite)
         {
            timeout -= amt;
         
            // check if a timeout has occurred
            if (timeout <= 0)
            {
               if (firstScreenOnly())
               {
                  reason = "timeout before any screen buffer became available";
               }
               else
               {
                  reason = "timeout before the specific screen buffer became " +
                           "available";
                           
                  if (lastReason != null)
                  {
                     // make a more specific failure reason using specific
                     // screen comparison data
                     reason = String.format("%s (%s)", reason, lastReason);
                  }
               }
            }
         }
      }
         
      if (reason == null)
      {
         status = Status.PASSED;
         
         updateDescription(true, screen);
      }
      else
      {
         // on a failure due to screen mis-matching, document the last screen
         if (failure)
         {
            updateDescription(false, screen);
         }
      }
      
      setStatus(status, reason);
   }
   
   /**
    * Cache the last failure reason in polling mode in case of a timeout so
    * that this data can be added to the generic timeout reason.
    *
    * @param    lastReason
    *           The last reason that a specific screen comparison failed.
    */
   protected void setLastReason(String lastReason)
   {
      this.lastReason = lastReason;
   }
   
   /**
    * Core logic to check the given screen.
    *
    * @param    term
    *           The terminal that represents the system under test.
    * @param    screen
    *           The current screen on the terminal.
    *
    * @return   The reason (if there was a failure) or <code>null</code> if
    *           the screens matched.
    */
   protected abstract String tryTest(Terminal term, char[][] screen);
   
   /**
    * Detect if the testing is complete when the first screen is tested.
    *
    * @return   <code>true</code> if the testable activity is complete when
    *           the first screen has been tested.
    */
   protected abstract boolean firstScreenOnly();
   
   /**
    * Store expected screen for reporting purposes
    *
    * @param    expectedScreen
    *           Expected screen as string
    */
   protected void setExpected(String expectedScreen)
   {
      this.expectedScreen = expectedScreen;
   }

   /**
    * Update the descriptive text for the testable activity by appending the
    * screen text.
    *
    * @param    passed
    *           <code>true</code> if the test passed.
    * @param    screen
    *           The current screen on the terminal.
    */
   private void updateDescription(boolean passed, char[][] screen)
   {
      StringBuilder sb = new StringBuilder();
      
      sb.append(getDescription())
        .append(' ')
        .append(passed ? "pass" : "fail")
        .append("ing screen = \n\n")
        .append(Terminal.screenToString(screen));
      if (!passed && expectedScreen != null)
      {
         sb.append("\n\nExpected screen:\n\n")
           .append(expectedScreen);
      }
      setDescription(sb.toString());
   }
   
   /**
    * Get the maximum number of milliseconds that this testable activity is allowed to run.
    * Valid for some of Tests only, -1 if not applicable
    * @return   The maximum number of milliseconds.
    */
   @Override
   public long getMaxElapsed()
   {
      return millis;
   }
}
