/*                                                                            *
 *   This file is part of the ESO X-Shooter package                           *
 *   Copyright (C) 2014 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 <xsh_utils.h>
#include <xsh_dfs.h>
#include <xsh_pfits.h>
#include <xsh_pfits_qc.h>
#include <xsh_data_spectrum.h>
#include <cpl.h>
#include <cpl_test.h>
#include <errno.h>

/*-----------------------------------------------------------------------------
                                Defines
 -----------------------------------------------------------------------------*/

#define TEST_NAME               "Test-XSH_DFS_SDP"

/* The following are input values for the dummy data. */
#define RECIPE_ID               "test_recipe"
#define RAW_FILE_NAME           "input_raw_file.fits"
#define RAW_FRAME_TAG           XSH_WAVE_VIS
#define PRODUCT_FILE_NAME       "OBJECT_SLIT_STARE_VIS.fits"
#define PRODUCT_FRAME_TAG       XSH_OBJECT_SLIT_STARE_VIS
#define OUTPUT_FILE_NAME        "SDP_"PRODUCT_FILE_NAME
#define OUTPUT_FRAME_TAG        "SCIENCE.SPECTRUM"
#define SNR_VALUE               3.5
#define TARGET_NAME             "SUN,centre,P2"
#define OBJECT_VALUE            "STD,TELLURIC"
#define SLIT_STRING_VALUE       "0.9x11"
#define MJDOBS_VALUE            10.1
#define RA_VALUE                60.605358
#define DEC_VALUE               -70.27194
#define ORIGIN_VALUE            "ESO"
#define TELESCOP_VALUE          "ESO-VLT-U2"
#define EQUINOX_VALUE           2000
#define RADECSYS_VALUE          "FK5"
#define INSTRUME_VALUE          "XSHOOTER"
#define EXPTIME_VALUE           15.
#define PROG_ID_VALUE           "abc"
#define OBS_ID_VALUE            123
#define PRO_TECH_VALUE          "ECHELLE,SLIT,STARE"
#define CONAD_VALUE             4.0
#define RON_VALUE               2.6
#define CSYER1_VALUE            1.2
#define BUINT_VALUE             "ADU"

/* The following are expected values used by the check_output_file() function
 * to check the generated output. */
#define EXPECTED_ORIGIN         ORIGIN_VALUE
#define EXPECTED_DISPELEM       "VIS"
#define EXPECTED_OBJECT         TARGET_NAME
#define EXPECTED_RADECSYS       RADECSYS_VALUE
#define EXPECTED_EQUINOX        EQUINOX_VALUE
#define EXPECTED_RA             RA_VALUE
#define EXPECTED_DEC            DEC_VALUE
#define EXPECTED_EXPTIME        EXPTIME_VALUE
#define EXPECTED_MJDOBS         MJDOBS_VALUE
#define EXPECTED_MJDEND         MJDOBS_VALUE + EXPTIME_VALUE/86400.
#define EXPECTED_PROG_ID        PROG_ID_VALUE
#define EXPECTED_OBS_ID         OBS_ID_VALUE
#define EXPECTED_OBSTECH        PRO_TECH_VALUE
#define EXPECTED_PRODCATG       OUTPUT_FRAME_TAG
#define EXPECTED_PRODLVL        2
#define EXPECTED_FLUXCAL        "UNCALIBRATED"
#define EXPECTED_WAVELMIN       500.
#define EXPECTED_WAVELMAX       600.
#define EXPECTED_SPEC_BIN       10.
#define EXPECTED_TOT_FLUX       CPL_FALSE
#define EXPECTED_FLUXERR        -2
#define EXPECTED_SNR            SNR_VALUE
#define EXPECTED_GAIN           CONAD_VALUE
#define EXPECTED_DETRON         RON_VALUE
#define EXPECTED_EFFRON         RON_VALUE
#define EXPECTED_PROV1          RAW_FILE_NAME
#define EXPECTED_NCOMBINE       1
#define EXPECTED_TITLE          "SUN,centre"
#define EXPECTED_APERTURE       11./3600.
#define EXPECTED_TELAPSE        EXPTIME_VALUE
#define EXPECTED_TMID           MJDOBS_VALUE + EXPTIME_VALUE/(2.0 * 86400.)
#define EXPECTED_SPEC_VAL       550.
#define EXPECTED_SPEC_BW        100.
#define EXPECTED_TDMIN1         EXPECTED_WAVELMIN
#define EXPECTED_TDMAX1         EXPECTED_WAVELMAX
#define EXPECTED_TFIELDS        5
#define EXPECTED_TFIELDS_UNCAL  7
#define EXPECTED_NELEM          11
#define EXPECTED_EXTNAME        "SPECTRUM"
#define EXPECTED_INHERIT        CPL_TRUE

/* The following are some additional FITS keyword macros. */
#define EQUINOX_KEYWORD         "EQUINOX"
#define RADECSYS_KEYWORD        "RADECSYS"
#define INSTRUME_KEYWORD        "INSTRUME"
#define SIMPLE_KEYWORD          "SIMPLE"
#define BITPIX_KEYWORD          "BITPIX"
#define EXTEND_KEYWORD          "EXTEND"
#define DATE_KEYWORD            "DATE"
#define CHECKSUM_KEYWORD        "CHECKSUM"
#define DATASUM_KEYWORD         "DATASUM"
#define TFIELDS_KEYWORD         "TFIELDS"

#define test_success(expr) cpl_test_assert((expr) == EXIT_SUCCESS)

/*----------------------------------------------------------------------------*/
/**
 * @defgroup xsh_dfs_sdp_test Testing of the SDP DFS functions.
 * @ingroup unit_tests
 */
/*----------------------------------------------------------------------------*/

/*-----------------------------------------------------------------------------
                            Private Function prototypes
 -----------------------------------------------------------------------------*/

static int getlen(const char* str);

static int create_dummy_raw_frame(cpl_frameset* frames);

static int create_input_product_frame(cpl_frameset* frames,
                                      const cpl_parameterlist* params,
                                      xsh_instrument* instrument);

static int check_output_file(cpl_boolean expect_uncalib);

/*----------------------------------------------------------------------------*/
/**
   @brief   Unit tests of the Science Data Product DFS functions.
**/
/*----------------------------------------------------------------------------*/

int main(void)
{
  cpl_test_init(PACKAGE_BUGREPORT, CPL_MSG_WARNING);
  cpl_msg_set_domain(TEST_NAME);

  /* Create a workspace sub directory for this unit test and change to that
   * directory. */
  /* Note: dont use the -p option of mkdir since it is less portable. */
  cpl_test_assert( system("test -d workspace_"TEST_NAME
                          " || mkdir workspace_"TEST_NAME) == 0 );
  cpl_test_assert( chdir("workspace_"TEST_NAME) == 0 );
  
  /* Force removal of the test files if any were left over from a previous
   * test run. */
  (void) remove(RAW_FILE_NAME);
  (void) remove(PRODUCT_FILE_NAME);
  (void) remove(OUTPUT_FILE_NAME);
  errno = 0;
  
  /* Create an empty parameter list. */
  cpl_parameterlist* params = cpl_parameterlist_new();
  cpl_test_assert(params != NULL);
  cpl_parameter* p = cpl_parameter_new_value(
                                "xsh."RECIPE_ID".dummy-association-keys",
                                CPL_TYPE_INT, "test param", RECIPE_ID, 2);
  cpl_test_assert(p != NULL);
  cpl_test_assert(cpl_parameterlist_append(params, p) == CPL_ERROR_NONE);
  
  /* Create a new empty frame set to which we add input and output frames. */
  cpl_frameset* frames = cpl_frameset_new();
  cpl_test_assert(frames != NULL);
  
  /* Create the dummy raw input FITS file and frame. */
  test_success(create_dummy_raw_frame(frames));
  
  /* Create an X-Shooter instrument structure required for adding the input
   * product frame and output frames to a frame set. */
  xsh_instrument* instrument = xsh_instrument_new();
  cpl_test_assert(instrument != NULL);
  xsh_instrument_set_mode(instrument, XSH_MODE_SLIT);
  xsh_instrument_set_arm(instrument, XSH_ARM_VIS);
  xsh_instrument_set_lamp(instrument, XSH_LAMP_QTH);
  xsh_instrument_set_recipe_id(instrument, RECIPE_ID);
  
  /* Create the dummy product spectrum FITS file and frame. */
  test_success(create_input_product_frame(frames, params, instrument));
  cpl_frame* product_frame = cpl_frameset_find(frames, PRODUCT_FRAME_TAG);
  cpl_test_assert(product_frame != NULL);
  
  /* Now test conversion of the product frame from X-Shooter native format to
   * the Science Data Product format for 1D spectra. */
  xsh_add_sdp_product_spectrum(NULL, product_frame, frames, frames,
                               params, RECIPE_ID, instrument);
  cpl_test_error(CPL_ERROR_NONE);
  
  test_success(check_output_file(CPL_FALSE));
  cpl_test_assert(cpl_test_get_failed() == 0);
  
  /* Check the conversion when data is given as flux calibrated. */
  xsh_add_sdp_product_spectrum(product_frame, NULL, frames, frames,
                               params, RECIPE_ID, instrument);
  cpl_test_error(CPL_ERROR_NONE);
  
  test_success(check_output_file(CPL_FALSE));
  cpl_test_assert(cpl_test_get_failed() == 0);
  
  /* Check the conversion when data is given as calibrated and uncalibrated. */
  xsh_add_sdp_product_spectrum(product_frame, product_frame, frames, frames,
                               params, RECIPE_ID, instrument);
  cpl_test_error(CPL_ERROR_NONE);
  
  test_success(check_output_file(CPL_TRUE));
  cpl_test_assert(cpl_test_get_failed() == 0);
  
  /* Cleanup memory and delete the generated test files if no tests failed. */
  xsh_instrument_free(&instrument);
  while (! cpl_frameset_is_empty(frames)) {
    cpl_frame* frame = cpl_frameset_get_position(frames, 0);
    cpl_test_assert(frame != NULL);
    cpl_test_assert(cpl_frameset_erase_frame(frames, frame) == CPL_ERROR_NONE);
  }
  cpl_frameset_delete(frames);
  cpl_parameterlist_delete(params);
  xsh_free_temporary_files();
  xsh_free_product_files();
  if (cpl_test_get_failed() == 0) {
    (void) remove(RAW_FILE_NAME);
    (void) remove(PRODUCT_FILE_NAME);
    (void) remove(OUTPUT_FILE_NAME);
    /* Delete workspace directory if it exists and no errors were discovered. */
    cpl_test_assert( chdir("..") == 0 );
    cpl_test_assert( system("test -d workspace_"TEST_NAME
                            "&& rm -r -f workspace_"TEST_NAME) == 0 );
  }

  return cpl_test_end(0);
}


/**
 * @internal
 * @brief Creates an input FITS file representing a dummy raw data file.
 *
 * A dummy raw input file is created and an appropriate frame is added to the
 * output frame set.
 *
 * @param[out] frames  The frame set that will be updated with the newly
 *                     created frame.
 * @return @c EXIT_SUCCESS if the file and frame were created successfully and
 *      testing can continue. If there was an error and testing cannot continue
 *      then @c EXIT_FAILURE is returned.
 */
static int create_dummy_raw_frame(cpl_frameset* frames)
{
  /* Create a property list of FITS keywords for the raw file. */
  cpl_propertylist* props = cpl_propertylist_new();
  cpl_test_assert(props != NULL);
  cpl_test_assert(cpl_propertylist_append_string(props, CPL_DFS_PRO_CATG,
                                                 RAW_FRAME_TAG)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(props, XSH_SDP_KEYWORD_OBJECT,
                                                 OBJECT_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(props, XSH_OBS_TARG_NAME,
                                                 TARGET_NAME)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(props, XSH_SLIT_VIS,
                                                 SLIT_STRING_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_double(props, XSH_MJDOBS,
                                                 MJDOBS_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_double(props, XSH_RA, RA_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_double(props, XSH_DEC, DEC_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(props, XSH_SDP_KEYWORD_ORIGIN,
                                                 ORIGIN_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(props, XSH_TELESCOP,
                                                 TELESCOP_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(props, INSTRUME_KEYWORD,
                                                 INSTRUME_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_double(props, EQUINOX_KEYWORD,
                                                 EQUINOX_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(props, RADECSYS_KEYWORD,
                                                 RADECSYS_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_double(props, XSH_EXPTIME,
                                                 EXPTIME_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(props, XSH_OBS_PROG_ID,
                                                 PROG_ID_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_int(props, XSH_OBS_ID, OBS_ID_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(props, XSH_PRO_TECH,
                                                 PRO_TECH_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_double(props, XSH_CONAD, CONAD_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_double(props, XSH_RON, RON_VALUE)
                  == CPL_ERROR_NONE);
  
  /* Create a dummy image and save it to a FITS file. */
  cpl_image* image = cpl_image_new(10, 10, CPL_TYPE_FLOAT);
  cpl_test_assert(image != NULL);
  cpl_test_assert(cpl_image_save(image, RAW_FILE_NAME, CPL_TYPE_FLOAT, props,
                                 CPL_IO_CREATE)
                  == CPL_ERROR_NONE);
  
  /* Create a frame to describe the FITS file and add it to the frame set. */
  cpl_frame* frame = cpl_frame_new();
  cpl_test_assert(frame != NULL);
  cpl_test_assert(cpl_frame_set_filename(frame, RAW_FILE_NAME)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_frame_set_tag(frame, RAW_FRAME_TAG)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_frame_set_type(frame, CPL_FRAME_TYPE_IMAGE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_frame_set_group(frame, CPL_FRAME_GROUP_RAW)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_frame_set_level(frame, CPL_FRAME_LEVEL_FINAL)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_frameset_insert(frames, frame) == CPL_ERROR_NONE);
  
  cpl_image_delete(image);
  cpl_propertylist_delete(props);
  return EXIT_SUCCESS;
}


/**
 * @internal
 * @brief Creates a 1D spectrum product FITS file in X-Shooter native format.
 *
 * A file containing a 1D spectrum in X-Shooter native format is created. In
 * addition an appropriate frame is added to the output frame set for the newly
 * created file.
 *
 * @param[out] frames  The output frame set to which we add the new frame.
 * @param[in] params  List of recipe parameters.
 * @param[in] instrument  The X-Shooter instrument structure describing various
 *      parameters of the instrument.
 *
 * @return @c EXIT_SUCCESS if the file and frame were created successfully and
 *      testing can continue. If there was an error and testing cannot continue
 *      then @c EXIT_FAILURE is returned.
 */
static int create_input_product_frame(cpl_frameset* frames,
                                      const cpl_parameterlist* params,
                                      xsh_instrument* instrument)
{
  /* Create the input FITS file containing a dummy spectrum in X-Shooter native
   * format. */
  xsh_spectrum* spectrum = xsh_spectrum_1D_create(500., 600., 10.);
  cpl_test_assert(spectrum != NULL);
  cpl_test_assert(cpl_image_set(spectrum->flux, 4, 1, 7) == CPL_ERROR_NONE);
  cpl_test_assert(cpl_image_set(spectrum->flux, 5, 1, 21) == CPL_ERROR_NONE);
  cpl_test_assert(cpl_image_set(spectrum->flux, 6, 1, 12) == CPL_ERROR_NONE);
  cpl_test_assert(cpl_image_set(spectrum->errs, 4, 1, 0.25) == CPL_ERROR_NONE);
  cpl_test_assert(cpl_image_set(spectrum->errs, 5, 1, 0.5) == CPL_ERROR_NONE);
  cpl_test_assert(cpl_image_set(spectrum->errs, 6, 1, 0.25) == CPL_ERROR_NONE);
  cpl_test_assert(cpl_image_set(spectrum->qual, 1, 1, 16) == CPL_ERROR_NONE);
  cpl_test_assert(cpl_image_set(spectrum->qual, 10, 1, 33) == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_double(spectrum->flux_header,
                                                 XSH_EXPTIME, EXPTIME_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_double(spectrum->flux_header,
                                                 XSH_QC_FLUX_SN, SNR_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_double(spectrum->flux_header,
                                                 XSH_CSYER1, CSYER1_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(spectrum->flux_header,
                                                 XSH_PRO_TECH, PRO_TECH_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(spectrum->flux_header,
                                                 XSH_BUNIT, BUINT_VALUE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_propertylist_append_string(spectrum->errs_header,
                                                 XSH_BUNIT, BUINT_VALUE)
                  == CPL_ERROR_NONE);
  cpl_frame* frame = xsh_spectrum_save(spectrum, PRODUCT_FILE_NAME,
                                       PRODUCT_FRAME_TAG);
  cpl_test_assert(frame != NULL);
  cpl_test_error(CPL_ERROR_NONE);
  
  /* Update some of the frame information. */
  cpl_test_assert(cpl_frame_set_tag(frame, PRODUCT_FRAME_TAG)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_frame_set_type(frame, CPL_FRAME_TYPE_IMAGE)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_frame_set_group(frame, CPL_FRAME_GROUP_PRODUCT)
                  == CPL_ERROR_NONE);
  cpl_test_assert(cpl_frame_set_level(frame, CPL_FRAME_LEVEL_FINAL)
                  == CPL_ERROR_NONE);
  
  /* Add the input spectrum frame to the list of frames. */
  xsh_add_product_spectrum(frame, frames, params, RECIPE_ID, instrument, NULL);
  cpl_test_error(CPL_ERROR_NONE);
  
  cpl_frame_delete(frame);
  xsh_spectrum_free(&spectrum);
  return EXIT_SUCCESS;
}


/**
 * @internal
 * @param str  Null terminated string. The pointer can be NULL.
 * @return The string's length or -1 if @a str was NULL.
 */
static int getlen(const char* str)
{
  if (str == NULL) return -1;
  return strlen(str);
}


/**
 * @internal
 * @brief Checks that the generated output file is valid.
 * @param expect_uncalib  Boolean value indicating if columns for the
 *      uncalibrated spectrum are expected and should be checked.
 * @return @c EXIT_SUCCESS if the test can continue and @c EXIT_FAILURE
 *      otherwise.
 */
static int check_output_file(cpl_boolean expect_uncalib)
{
  cpl_size i;
  int expected_TFIELDS = expect_uncalib ? EXPECTED_TFIELDS_UNCAL
                                        : EXPECTED_TFIELDS;

  /* Load the FITS keywords and binary table. */
  cpl_propertylist* props = cpl_propertylist_load(OUTPUT_FILE_NAME, 0);
  cpl_test_assert(props != NULL);
  cpl_table* table = cpl_table_load(OUTPUT_FILE_NAME, 1, CPL_TRUE);
  cpl_test_assert(table != NULL);
  cpl_propertylist* tableprops = cpl_propertylist_load(OUTPUT_FILE_NAME, 1);
  cpl_test_assert(tableprops != NULL);
  
  /* Check that the required keywords are present and have the expected
   * values in the primary header. */
  cpl_test_eq(cpl_propertylist_get_bool(props, SIMPLE_KEYWORD), CPL_TRUE);
  cpl_test(cpl_propertylist_get_int(props, BITPIX_KEYWORD) > 0);
  cpl_test_eq(cpl_propertylist_get_int(props, XSH_NAXIS), 0);
  cpl_test_eq(cpl_propertylist_get_bool(props, EXTEND_KEYWORD), CPL_TRUE);
  cpl_test_eq_string(cpl_propertylist_get_string(props, XSH_SDP_KEYWORD_ORIGIN),
                     EXPECTED_ORIGIN);
  cpl_test(getlen(cpl_propertylist_get_string(props, DATE_KEYWORD)) > 8);
  cpl_test(getlen(cpl_propertylist_get_string(props, XSH_TELESCOP)) > 1);
  cpl_test(getlen(cpl_propertylist_get_string(props, INSTRUME_KEYWORD)) > 1);
  cpl_test_eq_string(cpl_propertylist_get_string(props,
                                                 XSH_SDP_KEYWORD_DISPELEM),
                                                 EXPECTED_DISPELEM);
  cpl_test(getlen(cpl_propertylist_get_string(props,
                                              XSH_SDP_KEYWORD_SPECSYS)) > 1);
  cpl_test_eq_string(cpl_propertylist_get_string(props, XSH_SDP_KEYWORD_OBJECT),
                     EXPECTED_OBJECT);
  cpl_test_abs(cpl_propertylist_get_double(props, EQUINOX_KEYWORD),
               EXPECTED_EQUINOX, FLT_EPSILON);
  cpl_test_eq_string(cpl_propertylist_get_string(props, RADECSYS_KEYWORD),
                     EXPECTED_RADECSYS);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_RA), EXPECTED_RA,
               FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_DEC), EXPECTED_DEC,
               FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_EXPTIME),
               EXPECTED_EXPTIME, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_MJDOBS), EXPECTED_MJDOBS,
               FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_MJDEND), EXPECTED_MJDEND,
               FLT_EPSILON);
  cpl_test(cpl_propertylist_get_double(props, XSH_MJDOBS)
           < cpl_propertylist_get_double(props, XSH_MJDEND));
  cpl_test_eq_string(cpl_propertylist_get_string(props,
                                                 XSH_SDP_KEYWORD_PROG_ID),
                     EXPECTED_PROG_ID);
  cpl_test_eq(cpl_propertylist_get_int(props, XSH_SDP_KEYWORD_OBID1),
              EXPECTED_OBS_ID);
  cpl_test_eq(cpl_propertylist_get_bool(props, XSH_SDP_KEYWORD_M_EPOCH),
              CPL_FALSE);
  cpl_test_eq_string(cpl_propertylist_get_string(props,
                                                 XSH_SDP_KEYWORD_OBSTECH),
                     EXPECTED_OBSTECH);
  cpl_test_eq_string(cpl_propertylist_get_string(props,
                                                 XSH_SDP_KEYWORD_PRODCATG),
                     EXPECTED_PRODCATG);
  cpl_test_eq(cpl_propertylist_get_int(props, XSH_SDP_KEYWORD_PRODLVL),
              EXPECTED_PRODLVL);
  cpl_test_eq_string(cpl_propertylist_get_string(props,
                                                 XSH_SDP_KEYWORD_FLUXCAL),
                     EXPECTED_FLUXCAL);
  cpl_test_eq(cpl_propertylist_get_bool(props, XSH_SDP_KEYWORD_CONTNORM),
              CPL_FALSE);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_SDP_KEYWORD_WAVELMIN),
               EXPECTED_WAVELMIN, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_SDP_KEYWORD_WAVELMAX),
               EXPECTED_WAVELMAX, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_SDP_KEYWORD_SPEC_BIN),
               EXPECTED_SPEC_BIN, FLT_EPSILON);
  cpl_test_eq(cpl_propertylist_get_bool(props, XSH_SDP_KEYWORD_TOT_FLUX),
              EXPECTED_TOT_FLUX);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_SDP_KEYWORD_FLUXERR),
               EXPECTED_FLUXERR, FLT_EPSILON);
  cpl_test_eq(getlen(cpl_propertylist_get_string(props,
                                                 XSH_SDP_KEYWORD_REFERENC)),
              0);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_SNR),
               EXPECTED_SNR, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_SDP_KEYWORD_GAIN),
               EXPECTED_GAIN, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_SDP_KEYWORD_DETRON),
               EXPECTED_DETRON, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(props, XSH_SDP_KEYWORD_EFFRON),
               EXPECTED_EFFRON, FLT_EPSILON);
  cpl_test(cpl_propertylist_get_int(props, XSH_SDP_KEYWORD_LAMNLIN) > 1);
  cpl_test(cpl_propertylist_get_double(props, XSH_SDP_KEYWORD_LAMRMS) > 1e-6);
  cpl_test(cpl_propertylist_get_double(props, XSH_SDP_KEYWORD_SPEC_RES) > 1e-6);
  cpl_test(cpl_propertylist_get_double(props, XSH_SDP_KEYWORD_SPEC_ERR) > 1e-6);
  cpl_test(cpl_propertylist_get_double(props, XSH_SDP_KEYWORD_SPEC_SYE) > 1e-6);
  cpl_test(getlen(cpl_propertylist_get_string(props, XSH_SDP_KEYWORD_PROCSOFT))
                  > 1);
  cpl_test_eq_string(cpl_propertylist_get_string(props,
                                                 XSH_SDP_KEYWORD_PROV(1)),
                     EXPECTED_PROV1);
  cpl_test(cpl_propertylist_has(props, XSH_SDP_KEYWORD_PROV(2)) == CPL_FALSE);
  cpl_test_eq(cpl_propertylist_get_int(props, XSH_SDP_KEYWORD_NCOMBINE),
              EXPECTED_NCOMBINE);
  cpl_test(cpl_propertylist_has(props, "ASSON1"));
  cpl_test(cpl_propertylist_has(props, "ASSOC1"));
  cpl_test(cpl_propertylist_has(props, "ASSOM1"));
  cpl_test(cpl_propertylist_has(props, "ASSON2"));
  cpl_test(cpl_propertylist_has(props, "ASSOC2"));
  cpl_test(cpl_propertylist_has(props, "ASSOM2"));
  cpl_test(! cpl_propertylist_has(props, "ASSON3"));
  cpl_test(! cpl_propertylist_has(props, "ASSOC3"));
  cpl_test(! cpl_propertylist_has(props, "ASSOM3"));

  if (cpl_version_get_binary_version() >= CPL_VERSION(6, 4, 2)) {
    cpl_test(cpl_propertylist_has(props, CHECKSUM_KEYWORD));
    cpl_test(cpl_propertylist_has(props, DATASUM_KEYWORD));
  }

  /* Check that the table header keywords are correct. */
  cpl_test_eq(cpl_propertylist_get_int(tableprops, BITPIX_KEYWORD), 8);
  cpl_test_eq(cpl_propertylist_get_int(tableprops, XSH_NAXIS), 2);
  cpl_test_eq_string(cpl_propertylist_get_string(tableprops,
                                                 XSH_SDP_KEYWORD_TITLE),
                     EXPECTED_TITLE);
  cpl_test_eq_string(cpl_propertylist_get_string(tableprops,
                                                 XSH_SDP_KEYWORD_OBJECT),
                     EXPECTED_OBJECT);
  cpl_test_abs(cpl_propertylist_get_double(tableprops, XSH_RA), EXPECTED_RA,
               FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(tableprops, XSH_DEC), EXPECTED_DEC,
               FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(tableprops,
                                           XSH_SDP_KEYWORD_APERTURE),
               EXPECTED_APERTURE, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(tableprops, XSH_SDP_KEYWORD_TELAPSE),
               EXPECTED_TELAPSE, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(tableprops, XSH_SDP_KEYWORD_TMID),
               EXPECTED_TMID, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(tableprops,
                                           XSH_SDP_KEYWORD_SPEC_VAL),
               EXPECTED_SPEC_VAL, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(tableprops, XSH_SDP_KEYWORD_SPEC_BW),
               EXPECTED_SPEC_BW, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(tableprops,
                                           XSH_SDP_KEYWORD_TDMIN(1)),
               EXPECTED_TDMIN1, FLT_EPSILON);
  cpl_test_abs(cpl_propertylist_get_double(tableprops,
                                           XSH_SDP_KEYWORD_TDMAX(1)),
               EXPECTED_TDMAX1, FLT_EPSILON);
  cpl_test_eq(cpl_propertylist_get_int(tableprops, TFIELDS_KEYWORD),
              expected_TFIELDS);
  cpl_test_eq(cpl_propertylist_get_int(tableprops, XSH_SDP_KEYWORD_NELEM),
              EXPECTED_NELEM);
  cpl_test_eq_string(cpl_propertylist_get_string(tableprops, XSH_EXTNAME),
                     EXPECTED_EXTNAME);
  cpl_test_eq(cpl_propertylist_get_bool(tableprops, XSH_SDP_KEYWORD_INHERIT),
              EXPECTED_INHERIT);
/*FIXME: disabling CHECKSUM generation completely until whe know which version of CPL is fixed.
  if (cpl_version_get_binary_version() > CPL_VERSION(6, 4, 0)) {
    cpl_test(cpl_propertylist_has(tableprops, CHECKSUM_KEYWORD));
    cpl_test(cpl_propertylist_has(tableprops, DATASUM_KEYWORD));
  }
*/
  
  for (i = 1; i <= expected_TFIELDS; ++i) {
    char* field = cpl_sprintf("TTYPE%"CPL_SIZE_FORMAT, i);
    cpl_test(getlen(cpl_propertylist_get_string(tableprops, field)) > 1);
    cpl_free(field);
    field = cpl_sprintf("TFORM%"CPL_SIZE_FORMAT, i);
    cpl_test(getlen(cpl_propertylist_get_string(tableprops, field)) > 1);
    cpl_free(field);
    field = cpl_sprintf("TUNIT%"CPL_SIZE_FORMAT, i);
    switch (i) {
      case 1:
        cpl_test_eq_string(cpl_propertylist_get_string(tableprops, field),
                           "nm");
        break;
      case 2:
      case 3:
      case 6:
      case 7:
        cpl_test_eq_string(cpl_propertylist_get_string(tableprops, field),
                           "adu");
        break;
      case 4:
      case 5:
        cpl_test(getlen(cpl_propertylist_get_string(tableprops, field)) == 0);
        break;
      default:
        cpl_test(getlen(cpl_propertylist_get_string(tableprops, field)) > 1);
        break;
    }
    cpl_free(field);
    field = cpl_sprintf("TUTYP%"CPL_SIZE_FORMAT, i);
    cpl_test(getlen(cpl_propertylist_get_string(tableprops, field)) > 1);
    cpl_free(field);
    field = cpl_sprintf("TUCD%"CPL_SIZE_FORMAT, i);
    cpl_test(getlen(cpl_propertylist_get_string(tableprops, field)) > 1);
    cpl_free(field);
  }
  
  /* Check that the table values are correct. */
  cpl_test_eq(cpl_table_get_nrow(table), 1);
  cpl_test_eq(cpl_table_get_ncol(table), expected_TFIELDS);
  double expected_wave[] = {
      500, 510, 520, 530, 540, 550, 560, 570, 580, 590, 600
    };
  double expected_flux[] = { 0, 0, 0,  7   , 21  , 12   , 0, 0, 0,  0, 0};
  double expected_errs[] = { 0, 0, 0,  0.25,  0.5,  0.25, 0, 0, 0,  0, 0};
  int    expected_qual[] = {16, 0, 0,  0   ,  0  ,  0   , 0, 0, 0, 33, 0};
  double expected_snr[]  = { 0, 0, 0, 28   , 42  , 48   , 0, 0, 0,  0, 0};
  const cpl_array * wave_array = cpl_table_get_array(table,
                                                     XSH_SDP_COLUMN_WAVE, 0);
  cpl_test_nonnull(wave_array);
  const cpl_array * flux_array = cpl_table_get_array(table,
                                                     XSH_SDP_COLUMN_FLUX, 0);
  cpl_test_nonnull(flux_array);
  const cpl_array * errs_array = cpl_table_get_array(table,
                                                     XSH_SDP_COLUMN_ERR, 0);
  cpl_test_nonnull(errs_array);
  const cpl_array * qual_array = cpl_table_get_array(table,
                                                     XSH_SDP_COLUMN_QUAL, 0);
  cpl_test_nonnull(qual_array);
  const cpl_array * snr_array  = cpl_table_get_array(table,
                                                     XSH_SDP_COLUMN_SNR, 0);
  cpl_test_nonnull(snr_array);
  const double* calculated_wave = NULL;
  if (wave_array != NULL) {
    calculated_wave = cpl_array_get_data_double_const(wave_array);
    cpl_test_nonnull(calculated_wave);
  }
  const double* calculated_flux = NULL;
  if (flux_array != NULL) {
    calculated_flux = cpl_array_get_data_double_const(flux_array);
    cpl_test_nonnull(calculated_flux);
  }
  const double* calculated_errs = NULL;
  if (errs_array != NULL) {
    calculated_errs = cpl_array_get_data_double_const(errs_array);
    cpl_test_nonnull(calculated_errs);
  }
  const int* calculated_qual = NULL;
  if (qual_array != NULL) {
    calculated_qual = cpl_array_get_data_int_const(qual_array);
    cpl_test_nonnull(calculated_qual);
  }
  const double* calculated_snr = NULL;
  if (snr_array != NULL) {
    calculated_snr = cpl_array_get_data_double_const(snr_array);
    cpl_test_nonnull(calculated_snr);
  }
  if (calculated_wave != NULL && calculated_flux != NULL &&
      calculated_errs != NULL && calculated_qual != NULL &&
      calculated_snr != NULL)
  {
    for (i = 0; i < 11; ++i) {
      cpl_test_abs(calculated_wave[i], expected_wave[i], FLT_EPSILON);
      cpl_test_abs(calculated_flux[i], expected_flux[i], FLT_EPSILON);
      cpl_test_abs(calculated_errs[i], expected_errs[i], FLT_EPSILON);
      cpl_test_eq(calculated_qual[i], expected_qual[i]);
      cpl_test_abs(calculated_snr[i], expected_snr[i], FLT_EPSILON);
    }
  }
  
  /* Check extra columns in table if uncalibrated data is expected. */
  if (expect_uncalib) {
    flux_array = cpl_table_get_array(table, XSH_SDP_COLUMN_FLUX_REDUCED, 0);
    cpl_test_nonnull(flux_array);
    errs_array = cpl_table_get_array(table, XSH_SDP_COLUMN_ERR_REDUCED, 0);
    cpl_test_nonnull(errs_array);
    calculated_flux = NULL;
    if (flux_array != NULL) {
      calculated_flux = cpl_array_get_data_double_const(flux_array);
      cpl_test_nonnull(calculated_flux);
    }
    calculated_errs = NULL;
    if (errs_array != NULL) {
      calculated_errs = cpl_array_get_data_double_const(errs_array);
      cpl_test_nonnull(calculated_errs);
    }
    if (calculated_flux != NULL && calculated_errs != NULL)
    {
      for (i = 0; i < 11; ++i) {
        cpl_test_abs(calculated_flux[i], expected_flux[i], FLT_EPSILON);
        cpl_test_abs(calculated_errs[i], expected_errs[i], FLT_EPSILON);
      }
    }
  }
  
  /* Cleanup memory */
  cpl_table_delete(table);
  cpl_propertylist_delete(tableprops);
  cpl_propertylist_delete(props);
  
  return EXIT_SUCCESS;
}
