/*                                                                              *
 *   This file is part of the ESO FORS   package                                *
 *   Copyright (C) 2004,2005 European Southern Observatory                      *
 *                                                                              *
 *   This library is free software; you can redistribute it and/or modify       *
 *   it under the terms of the GNU General Public License as published by       *
 *   the Free Software Foundation; either version 2 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 General Public License for more details.                               *
 *                                                                              *
 *   You should have received a copy of the GNU General Public License          *
 *   along with this program; if not, write to the Free Software                *
 *   Foundation, 51 Franklin St, Fifth Floor, Boston, MA  02111-1307  USA       *
 *                                                                              */
 
#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

/*-----------------------------------------------------------------------------
                                Includes
 -----------------------------------------------------------------------------*/

#include <test.h>

#include <fors_utils.h>

#include <cpl.h>
#include <math.h>  /* fabs() */
#include <string.h> /* strlen() */

/*----------------------------------------------------------------------------*/
/**
 * @defgroup test     Unit test infrastructure
 */
/*----------------------------------------------------------------------------*/

/*-----------------------------------------------------------------------------
                                Implementation
 -----------------------------------------------------------------------------*/

/**@{*/

/** The number of failures */
static unsigned long test_nfail = 0; 
static cpl_errorstate error_init;

/*----------------------------------------------------------------------------*/
/**
  @brief  Evaluate an expression and update an internal counter if it fails
  @param  expression  The expression to evaluate
  @param  message     The text message associated with the expression
  @param  function    function name
  @param  file        filename
  @param  line        line number
 */
/*----------------------------------------------------------------------------*/
static void
_test(int expression, const char *message,
     const char *function, const char *file, unsigned line)
{
    const char *error_state = (cpl_error_get_code() != CPL_ERROR_NONE) ?
        cpl_sprintf(" (CPL-error state: '%s' at %s)",
                       cpl_error_get_message(), cpl_error_get_where()) :
        NULL;
    
    if (expression) {
        cpl_msg_debug(function,
                      "OK at %s:%u%s: %s",
                      file, line, error_state != NULL ? error_state : "",
                      message);
    } else {
        if (test_nfail + 1 > test_nfail) {
            test_nfail++;
        }
        else {
            cpl_msg_error(function, "Number of errors (%lu) overflow!",
                          test_nfail);
        }
        
        cpl_msg_error(function,
                      "Failure at %s:%u%s: %s",
                      file, line, error_state != NULL ? error_state : "",
                      message);
    }

    if (error_state != NULL)
        {
            cpl_free((char *)error_state);
        }

    return;
}


/*----------------------------------------------------------------------------*/
/**
  @brief  Test a given expression
  @param  expression  the expression to evaluate
  @param  expr_string the expression to evaluate as a string
  @param  function    function name
  @param  file        filename
  @param  line        line number
  @note   A zero value of the expression is a failure, other values are not
 */
/*----------------------------------------------------------------------------*/
void 
test_macro(int expression, const char *expr_string,
                  const char *function, const char *file, unsigned line)
{
    const char *message = cpl_sprintf("(%s) = %d", expr_string, expression);

    _test(expression, message,
         function, file, line);

    cpl_free((char *)message);

    return;
}

/*----------------------------------------------------------------------------*/
/**
  @brief  Test if two integer expressions are equal
  @param  first            The first value in the comparison
  @param  first_string     The first value as a string
  @param  second           The second value in the comparison
  @param  second_string    The second value as a string
  @param  function         function name
  @param  file             filename
  @param  line             line number
  @note   This function should only be called from the macro TEST_ABS
 */
/*----------------------------------------------------------------------------*/
void 
test_eq_macro(int first,  const char *first_string,
                     int second, const char *second_string,
                     const char *function, const char *file, unsigned line)
{
    const char *message =
        cpl_sprintf("(%s) = %d; (%s) = %d",
                       first_string, first,
                       second_string, second);

    _test(first == second, message,
         function, file, line);

    cpl_free((char *)message);

    return;
}

/*----------------------------------------------------------------------------*/
/**
  @brief  Test if two strings are equal
  @param  first            The first value in the comparison
  @param  first_string     The first value as a string
  @param  second           The second value in the comparison
  @param  second_string    The second value as a string
  @param  function         function name
  @param  file             filename
  @param  line             line number
  @note   This function should only be called from the macro TEST_ABS
 */
/*----------------------------------------------------------------------------*/
void 
test_eq_string_macro(const char *first,  const char *first_string,
			    const char *second, const char *second_string,
			    const char *function, 
			    const char *file, unsigned line)
{
    const char *message;

    message = cpl_sprintf("%s = '%s'; %s = '%s'",
                             first_string, first != NULL ? first : "NULL",
                             second_string, second != NULL ? second : "NULL");

    _test(first != NULL && second != NULL && strcmp(first, second) == 0, 
         message,
         function, file, line);

    cpl_free((char *)message);

    return;
}

/*----------------------------------------------------------------------------*/
/**
  @brief  Test if two numerical expressions are 
          within a given (absolute) tolerance

  @param  first            The first value in the comparison
  @param  first_string     The first value as a string
  @param  second           The second value in the comparison
  @param  second_string    The second value as a string
  @param  tolerance        A non-negative tolerance
  @param  tolerance_string The tolerance as a string
  @param  function         function name
  @param  file             filename
  @param  line             line number
  @note   This function should only be called from the macro TEST_ABS
 */
/*----------------------------------------------------------------------------*/
void 
test_abs_macro(double first,  const char *first_string,
                      double second, const char *second_string,
                      double tolerance, const char *tolerance_string,
                      const char *function, const char *file, unsigned line)
{
    const char *message =
        cpl_sprintf("|%s - (%s)| = |%g - (%g)| <= %g = %s",
                       first_string, second_string, first, second,
                       tolerance, tolerance_string);

    _test(fabs(first - second) <= tolerance, message,
         function, file, line);
    /* Note: fails if tolerance is negative */

    cpl_free((char *)message);

    return;
}

/*----------------------------------------------------------------------------*/
/**
  @brief  Test if two numerical expressions are 
          within a given relative tolerance

  @param  first            The first value in the comparison
  @param  first_string     The first value as a string
  @param  second           The second value in the comparison
  @param  second_string    The second value as a string
  @param  tolerance        A non-negative tolerance
  @param  tolerance_string The tolerance as a string
  @param  function         function name
  @param  file             filename
  @param  line             line number
  @note   This function should only be called from the macro TEST_REL
 */
/*----------------------------------------------------------------------------*/
void 
test_rel_macro(double first,  const char *first_string,
                      double second, const char *second_string,
                      double tolerance, const char *tolerance_string,
                      const char *function, const char *file, unsigned line)
{
    const char *message;
    
    if (first == 0 || second == 0) {
        /* Division by zero -> fail */
        message = cpl_sprintf("%s = %g; %s = %g (division by zero)",
                                 first_string, first,
                                 second_string, second);
        _test(0, message,
             function, file, line);
    }
    else {
        message = 
            cpl_sprintf("|%s - (%s)|/|%s| = |%g - (%g)|/|%g| <= %g = %s and "
                           "|%s - (%s)|/|%s| = |%g - (%g)|/|%g| <= %g = %s",
                           first_string, second_string, first_string,
                           first, second, first, tolerance, tolerance_string,
                           first_string, second_string, second_string,
                           first, second, second, tolerance, tolerance_string);            
        
        _test(fabs((first - second)/first ) <= tolerance &&
             fabs((first - second)/second) <= tolerance, message,
             function, file, line);
        /* Note: fails if tolerance is negative */
    }

    cpl_free((char *)message);

    return;
}


/*----------------------------------------------------------------------------*/
/**
  @brief   Initialize CPL + messaging
  @param   file     filename
  @note    This function should be called only by using the macro
           TEST_INIT
 */
/*----------------------------------------------------------------------------*/
void
test_init_macro(const char *file)
{
    /* Build message domain + log filename
       from source file name */
    
    unsigned len = strlen(file);
    int able_to_parse_filename;

    cpl_init(CPL_INIT_DEFAULT);
    error_init = cpl_errorstate_get();
    srand(0);

    if (len < strlen("s.c") || strcmp(file + len - strlen(".c"), ".c") != 0)
        {
            able_to_parse_filename = 0;
            /* Here, we should issue a warning but we need to
               initialize the CPL message system first */
        }
    else
        {
            /* Form domain from filename by removing ".c" suffix
               and stripping any path prefix */

            const char dir_delimiter = '/'; /* UNIX only! */
            unsigned path_length = 0;       /* Length of path prefix 
                                               including final '/' */
            int i;

            for (i = len - strlen(".c") - 1; i >= 0; i--)
                {
                    if (file[i] == dir_delimiter && path_length == 0)
                        {
                            path_length = i + 1;
                        }
                }
            
            able_to_parse_filename = (len - path_length - strlen(".c") >= 1);
            if (able_to_parse_filename)
                {
                    const char *logfile;
                    char *domain;

                    domain = cpl_strdup(file + path_length);
                    domain[len - path_length - strlen(".c")] = '\0';

                    cpl_msg_set_domain(domain);
                    
                    logfile = cpl_sprintf("%s.log", domain);
                    cpl_msg_set_log_name(logfile);
                    
                    cpl_free((char *)logfile);
                    cpl_free(domain);
                }
            /* else: Use default domain + logfilename */
        }

    /* Adopt the policy that the unit test is quiet
       unless something is wrong.
       Dump debugging info to the log file */
    cpl_msg_set_level(CPL_MSG_WARNING); 
    cpl_msg_set_log_level(CPL_MSG_DEBUG);
    
    if (!able_to_parse_filename)
        {
            cpl_msg_warning(cpl_func,
                            "Source file name '%s' does not match [base].c. ",
                            file);
        }

    if (cpl_error_get_code() != CPL_ERROR_NONE)
        {
            cpl_msg_error(cpl_func,
                          "Failure during initialization: %s at %s",
                          cpl_error_get_message(), cpl_error_get_where());
        }

    return;
}

/*----------------------------------------------------------------------------*/
/**
  @brief   Perform the final checks and return the number of errors
  @param   function         function name
  @param   file             filename
  @param   line             line number
  @return  The total number of errors in the tested module
  @note    This function should only be called from the macro TEST_END

 */
/*----------------------------------------------------------------------------*/
unsigned
test_end_macro(const char *function, const char *file, unsigned line)
{
    const int memory_is_empty = cpl_memory_is_empty();

    test_eq_macro(cpl_error_get_code(), "cpl_error_get_code()",
                  CPL_ERROR_NONE, "CPL_ERROR_NONE",
                  function, file, line);

    if (cpl_error_get_code() != CPL_ERROR_NONE) {
        cpl_errorstate_dump(error_init, CPL_FALSE, NULL);
    }

    test_macro(memory_is_empty,
               "memory_is_empty",
               function, file, line);
    
    if (!memory_is_empty) {
        cpl_msg_error(function, "Memory leak detected:");
        cpl_memory_dump(); 
    }

    cpl_end();

    return test_nfail;
}





#undef cleanup
#define cleanup \
do { \
    cpl_propertylist_delete(product_header); \
} while(0)
/*----------------------------------------------------------------------------*/
/**
  @brief   Test existence of recipe products
  @param   product_tags     array of expected product tags
  @param   n_prod           number of products
  @param   main_product     the only product which should contain QC parameters
  @param   qc               array of expected QC parameters
  @param   n_qc             number of QC parameters

 */
/*----------------------------------------------------------------------------*/
void 
test_recipe_output(const cpl_frameset *frames,
                   const char *const product_tags[], int n_prod,
		   const char *main_product,
		   const char *const qc[], int n_qc)
{
    cpl_propertylist *product_header = NULL;
    
    int i;
    for (i = 0; i < n_prod; i++) {
        const cpl_frame *product = cpl_frameset_find_const(frames, product_tags[i]);
        test( product != NULL );
        test_eq( cpl_frame_get_group(product), CPL_FRAME_GROUP_PRODUCT );
        
        cpl_propertylist_delete(product_header);
        product_header = cpl_propertylist_load(cpl_frame_get_filename(product), 0);
        
        assure( !cpl_error_get_code(), return, 
                "Failed to load product header" );
        
        
	if (strcmp(product_tags[i], main_product) != 0) {
	    test( !cpl_propertylist_has(product_header,
					"ESO QC DID") );
	}
	    
        int j;
        for (j = 0; j < n_qc; j++) {
	    
            const char *full_qc_name = cpl_sprintf("ESO %s", qc[j]);
	    
	    cpl_msg_debug(cpl_func, 
			  "Looking for '%s' in '%s'",
			  qc[j], product_tags[i]);
	    
	    if (strcmp(product_tags[i], main_product) == 0) {
		test( cpl_propertylist_has(product_header,
					   full_qc_name) );
	    }
	    else {
		test( !cpl_propertylist_has(product_header,
					   full_qc_name) );
	    }
	    
	    cpl_free((void *)full_qc_name);
        }
    }
    
    cleanup;
    return;
}

/**@}*/
