/*
** Module   : FileComparison.java
** Abstract : base class for functionality to compare two files
**
** Copyright (c) 2009-2021, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090218 Base class for functionality to compare two files.
** 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.
*/

/*
** 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.io.*;
import com.goldencode.io.*;
import com.goldencode.harness.*;
import com.goldencode.harness.transport.*;

/**
 * Base class for functionality to compare two files.
 */
public abstract class FileComparison
extends Report
implements Testable
{
   /** Format specification for the step description. */
   private static final String DESCR_SPEC = "baseline = '%s'; actual = '%s'; " +
                                            "downloadDir = '%s'; download = "  +
                                            "%b; excludes = [%s];";
                                            
   /** File containing the expected data. */
   protected String baseline = null;
   
   /** File containing the actual data. */
   protected String actual = null;
   
   /** The local directory in which a downloaded file will be placed. */
   protected String downloadDir = null;
   
   /** Helper for downloading the comparison file via SFTP. */
   protected SFTPSession sftp = null;
   
   /** Portions of the stream to ignore in comparisons. */
   protected ExclusionRegion[] excludes = null;
   
   /** Detailed reason for a failure. */
   protected String reason = null;
   
   /**
    * Create an instance.
    *
    * @param    name
    *           Short name of the testable activity.
    * @param    baseline
    *           Identifies the file containing the expected data.
    * @param    actual
    *           Identifies the file containing the actual data generated during
    *           the test.
    * @param    downloadDir 
    *           The local directory in which a downloaded file will be placed
    *           or <code>null</code> if no downloading should occur.
    * @param    sftp
    *           Helper for downloading the comparison file via SFTP. The
    *           <code>actual</code> parameter must specify the absolute or
    *           relative filename of the file on the server system. The file
    *           will be downloaded into the download directory as specified
    *           in the project index.
    * @param    excludes
    *           A list of portions of the byte stream to be ignored for
    *           comparison purposes or <code>null</code> if there are no
    *           exclusion regions.
    */
   public FileComparison(String            name,
                         String            baseline,
                         String            actual,
                         String            downloadDir,
                         SFTPSession       sftp,
                         ExclusionRegion[] excludes)
   {
      super(name, "");
      this.baseline    = baseline;
      this.actual      = actual;
      this.downloadDir = downloadDir;
      this.sftp        = sftp;
      this.excludes    = excludes;
      
      String txt = String.format(DESCR_SPEC,
                                 baseline,
                                 actual,
                                 (downloadDir == null) ? "none" : downloadDir,
                                 (sftp != null),
                                 dumpExcludes());
      setDescription(txt);
   }
   
   /**
    * Execute the directive of the test using the given terminal as the data
    * sink and/or source for the operation.
    */
   public void execute()
   {
      try
      {
         File base  = obtainFile(baseline, "Baseline");
         
         String local = actual;
         
         if (sftp != null)
         {
            // download the file
            local = download();
         }
         
         File other = obtainFile(local, "Comparison");
         
         if (reason == null)
         {         
            reason = compare(base, other);
         }
         
         setStatus((reason == null) ? Status.PASSED : Status.FAILED, reason);
      }
      
      catch (Exception excp)
      {
         throw new RuntimeException(excp);
      }
   }
   
   /**
    * Download the comparison file from the server via SFTP to a local file
    * in the download directory. If there is a failure, the cause will be
    * stored in the <code>reason</code> data member.
    *
    * @return   The downloaded filename (on the local system) or
    *           <code>null</code> if there was a failure.
    */
   public String download()
   {
      if (actual == null || actual.length() == 0)
      {
         reason = "Missing remote filename.";
         return null;
      }
      
      StringBuilder sb = new StringBuilder();
      
      // automatically target the download directory
      sb.append((downloadDir == null) ? "." : downloadDir);
      
      if (!downloadDir.endsWith(File.separator))
         sb.append(File.separator);
      
      // calculate the local filename (split at the last file separator if
      // one exists)
      int pos = actual.lastIndexOf(File.separator);
      sb.append((pos < 0) ? actual : actual.substring(pos + 1));
         
      String local = sb.toString();
      
      if (downloadDir != null)
      {
         if (!FileSystem.createPath(downloadDir))
         {
            reason = String.format("Failure in creation of directory '%s'.",
                                   downloadDir);
            return null;
         }
      }
      
      sftp.connect();
      sftp.get(actual, local);
      sftp.disconnect();
      
      return local;
   }
   
   /**
    * Detect if the actual data file generated during testing matches the
    * given baseline file outside of any specified exclusion regions.
    *
    * @param    base
    *           Baseline file data.
    * @param    other
    *           Comparison file data.
    *
    * @return   <code>null</code> if the actual file is the same as the
    *           baseline file (after ignoring any exclusion regions) or an
    *           error string on any deviation.
    * 
    * @throws   Exception
    *           if an error is encountered comparing files.
    */
   public abstract String compare(File base, File other)
   throws Exception;
   
   /**
    * Get the file instance represented by the given filename. If the file
    * system object referenced does not exist or is not a file, then the
    * <code>reason</code> member will be set to a non-null description.
    * 
    * @param    fname
    *           Name of file to obtain.
    * @param    descr
    *           Brief description of file.
    *
    * @return   The file object or <code>null</code> on any failure.
    */
   public File obtainFile(String fname, String descr)
   {
      File file = new File(fname);
      
      if (!file.exists())
      {
         reason = String.format("%s file '%s' doesn't exist.", descr, fname);
         return null;
      }
      
      if (!file.isFile())
      {
         reason = String.format("%s file '%s' is not a file.", descr, fname);
         return null;
      }
      
      return file;
   }
   
   /**
    * Convert the list of exclusion regions into a descriptive string.
    *
    * @return   The specific excluded regions or "none" if there are no
    *           configured regions.
    */
   private String dumpExcludes()
   {
      if (excludes == null)
      {
         return "none";
      }
      
      StringBuilder sb = new StringBuilder();
      
      for (ExclusionRegion region : excludes)
      {
         if (sb.length() > 0)
            sb.append(", ");
         
         sb.append(region.toString());
      }
      
      return sb.toString();
   }
}
