/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set sw=2 sts=2 et cin: */
/*
 * This file is part of the MUSE Instrument Pipeline
 * Copyright (C) 2007-2016 European Southern Observatory
 *
 * This program 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, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#define _BSD_SOURCE /* get setenv() from stdlib.h */
#include <stdlib.h> /* setenv() */
#include <string.h>

#include <muse.h>

#define MUSE_TEST_RESAMPLING_DELETE_FILES 1 /* should be 1 when not debugging */

/* symbolic property names we compare to */
enum {                       TMEAN = 0, TSTD, TMIN, TMAX, TMED, TMAD, TFLUX, TCENX, TCENY };
/* comparison values for minimum and maximum values of above properties, *
 * this version for the cube-related comparisons                         */
const double kComparisonMin[] = { 2.62, 5.75, 0.00, 0.00, 1.50, 1.40,  6550., 10.6, 6.75 },
             kComparisonMax[] = { 4.60, 7.85, 0.00, 0.00, 3.58, 3.50, 12000., 11.1, 6.95 },
             /* Lanczos results in pretty deviant values, and    *
              * especially high stdev in the collapsed images... */
             kComparisonMinLanczos[] = { 2.97, 5.79, 0.00, 0.00, 2.09, 1.44,  7450., 10.5, 6.79 },
             kComparisonMaxLanczos[] = { 4.86, 18.7, 0.00, 0.00, 3.59, 3.66, 12200., 11.0, 7.05 };
/* comparison values for minimum and maximum values of above properties *
 * this version for the image-related comparisons                         */
const double kComparisonImageMin[] = { 123., 444., -10., 3515., 0.00, 0.00, 5068950., 0.00, 0.00 },
             kComparisonImageMax[] = { 132., 470.,  -9., 3648., 0.00, 0.00, 5098805., 0.00, 0.00 };


static void
muse_resampling_test_cubeimage_stats(cpl_image *aImage, const char *aName)
{
  /* same tests as on central plane, collapsed image; use *
   * the full image and a window near the brightest start */
  cpl_stats *stats = cpl_stats_new_from_image(aImage, CPL_STATS_ALL),
            *statw = cpl_stats_new_from_image_window(aImage, CPL_STATS_CENTROID,
                                                     1, 1, 20, 13);
  cpl_msg_debug(__func__, "%s: average: %f +/- %f, median: %f +/- %f, flux: %f, "
                "centroid: %f %f", aName,
                cpl_stats_get_mean(stats), cpl_stats_get_stdev(stats),
                cpl_stats_get_median(stats), cpl_stats_get_median_dev(stats),
                cpl_stats_get_flux(stats),
                cpl_stats_get_centroid_x(statw), cpl_stats_get_centroid_y(statw));
  if (strstr(aName, " 5")) {
    /* special-case Lanczos resampled data because that amplifies extremes */
    cpl_test(cpl_stats_get_mean(stats) > kComparisonMinLanczos[TMEAN] &&
             cpl_stats_get_mean(stats) < kComparisonMaxLanczos[TMEAN]);
    cpl_test(cpl_stats_get_stdev(stats) > kComparisonMinLanczos[TSTD] &&
             cpl_stats_get_stdev(stats) < kComparisonMaxLanczos[TSTD]);
    cpl_test(cpl_stats_get_median(stats) > kComparisonMinLanczos[TMED] &&
             cpl_stats_get_median(stats) < kComparisonMaxLanczos[TMED]);
    cpl_test(cpl_stats_get_median_dev(stats) > kComparisonMinLanczos[TMAD] &&
             cpl_stats_get_median_dev(stats) < kComparisonMaxLanczos[TMAD]);
    cpl_test(cpl_stats_get_flux(stats) > kComparisonMinLanczos[TFLUX] &&
             cpl_stats_get_flux(stats) < kComparisonMaxLanczos[TFLUX]);
    cpl_test(cpl_stats_get_centroid_x(statw) > kComparisonMinLanczos[TCENX] &&
             cpl_stats_get_centroid_x(statw) < kComparisonMaxLanczos[TCENX]);
    cpl_test(cpl_stats_get_centroid_y(statw) > kComparisonMinLanczos[TCENY] &&
             cpl_stats_get_centroid_y(statw) < kComparisonMaxLanczos[TCENY]);
  } else {
    cpl_test(cpl_stats_get_mean(stats) > kComparisonMin[TMEAN] &&
             cpl_stats_get_mean(stats) < kComparisonMax[TMEAN]);
    cpl_test(cpl_stats_get_stdev(stats) > kComparisonMin[TSTD] &&
             cpl_stats_get_stdev(stats) < kComparisonMax[TSTD]);
    cpl_test(cpl_stats_get_median(stats) > kComparisonMin[TMED] &&
             cpl_stats_get_median(stats) < kComparisonMax[TMED]);
    cpl_test(cpl_stats_get_median_dev(stats) > kComparisonMin[TMAD] &&
             cpl_stats_get_median_dev(stats) < kComparisonMax[TMAD]);
    cpl_test(cpl_stats_get_flux(stats) > kComparisonMin[TFLUX] &&
             cpl_stats_get_flux(stats) < kComparisonMax[TFLUX]);
    cpl_test(cpl_stats_get_centroid_x(statw) > kComparisonMin[TCENX] &&
             cpl_stats_get_centroid_x(statw) < kComparisonMax[TCENX]);
    cpl_test(cpl_stats_get_centroid_y(statw) > kComparisonMin[TCENY] &&
             cpl_stats_get_centroid_y(statw) < kComparisonMax[TCENY]);
  }
  cpl_stats_delete(stats);
  cpl_stats_delete(statw);
} /* muse_resampling_test_cubeimage_stats() */

static cpl_error_code
muse_test_resampling_cube(muse_pixtable *aPtPix)
{
  cpl_msg_info(__func__, "using pixel table with %"CPL_SIZE_FORMAT" rows",
               muse_pixtable_get_nrow(aPtPix));
  /* set WFM in header, so that resampling with WCS works */
  cpl_propertylist_append_string(aPtPix->header, "ESO INS MODE", "WFM-NONAO-N");

  /* create built-in white-light filter, luckily it doesn't *
   * need the processing argument to "load" this one        */
  muse_table *fwhite = muse_table_load_filter(NULL, "white");

  /* duplicate the pixel table, project to sky, to also get resampling with WCS */
  muse_pixtable *ptdeg = muse_pixtable_duplicate(aPtPix);
  cpl_propertylist *wcs = muse_wcs_create_default();
  cpl_test(muse_wcs_project_tan(ptdeg, wcs) == CPL_ERROR_NONE);
  cpl_propertylist_delete(wcs);
  cpl_test(muse_wcs_position_celestial(ptdeg, muse_pfits_get_ra(ptdeg->header),
                                       muse_pfits_get_dec(ptdeg->header))
           == CPL_ERROR_NONE);
  muse_pixtable *pts[] = { aPtPix, ptdeg, NULL };

  muse_resampling_dispersion_type tl;
  for (tl = MUSE_RESAMPLING_DISP_AWAV; tl <= MUSE_RESAMPLING_DISP_WAVE_LOG; tl++) {
    muse_resampling_type t;
    for (t = MUSE_RESAMPLE_NEAREST; t < MUSE_RESAMPLE_NONE; t++) {
      muse_pixtable *pt;
      unsigned ipt;
      for (ipt = 0, pt = pts[ipt]; pt; pt = pts[++ipt]) {
        const char *unit = cpl_table_get_column_unit(pt->table, MUSE_PIXTABLE_XPOS);

        /* ====================================================================== *
         * First test is for the FITS NAXIS=3 format                              *
         * ====================================================================== */
        cpl_msg_info(__func__, "__________ Resampling pixel table [%s] to cube, "
                     "method %d, spectral type %d ___________", unit, t, tl);
        muse_resampling_params *params = muse_resampling_params_new(t);
        params->tlambda = tl;
        if (t == MUSE_RESAMPLE_WEIGHTED_LANCZOS) {
          params->ld = 3; /* want to test lanczos3 */
        } else if (t == MUSE_RESAMPLE_WEIGHTED_RENKA) { /* want to test how... */
          params->ld = -5; /* the resampling recovers from negative loop distance */
        }
        muse_pixgrid *grid, **gridptr = NULL;
        char *fnw = cpl_sprintf("cube_resampled_%s_l%d_%d_weight.fits", unit, tl, t);
        if (t == MUSE_RESAMPLE_WEIGHTED_DRIZZLE) { /* test weight cube creation... */
          cpl_test(setenv("MUSE_DEBUG_WEIGHT_CUBE", fnw, 1) == 0); /* for drizzle */
        }
        if (tl == MUSE_RESAMPLING_DISP_AWAV && t != MUSE_RESAMPLE_NEAREST) {
          /* test grid creation, used for direct collapsing below */
          gridptr = &grid;
        }
        muse_datacube *cube = muse_resampling_cube(pt, params, gridptr);
        if (t == MUSE_RESAMPLE_WEIGHTED_DRIZZLE) {
          /* try to re-load a plane from the weight cube back */
          cpl_image *plane = cpl_image_load(fnw, CPL_TYPE_UNSPECIFIED, 5, 0);
          cpl_test_nonnull(plane);
          double mean = cpl_image_get_mean(plane),
                 min = cpl_image_get_min(plane);
          cpl_msg_debug(__func__, "weight image mean = %f", mean);
          cpl_test(mean > 0.3 && mean < 1.0); /* some rough guess */
          cpl_test(min >= 0.); /* zero or positive weights */
          int xsize = cpl_image_get_size_x(plane),
              ysize = cpl_image_get_size_y(plane);
          cpl_image_delete(plane);
          cpl_test(unsetenv("MUSE_DEBUG_WEIGHT_CUBE") == 0);
#if MUSE_TEST_RESAMPLING_DELETE_FILES
          cpl_test(remove(fnw) == 0); /* can delete the file again */
#endif

          /* test muse_pixgrid_create() for a few cases of unusual sizes */
          cpl_errorstate state;
          int ig;
          for (ig = 0; ig < 20; ig++) {
            cpl_msg_debug(__func__, "unusual z-size %d", ig);
            state = cpl_errorstate_get();
            muse_pixgrid *gridtest = muse_pixgrid_create(pt, cube->header,
                                                         xsize, ysize, ig);
            if (ig == 0) {
              cpl_test(!cpl_errorstate_is_equal(state) &&
                       cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
              cpl_errorstate_set(state);
              cpl_test_null(gridtest);
              continue;
            }
            cpl_test(cpl_errorstate_is_equal(state));
            cpl_test_nonnull(gridtest);
            muse_pixgrid_delete(gridtest);
          } /* for ig (some unsual z-axis sizes) */
          /* while we are here, also check other failure cases of muse_pixgrid_create() */
          if (tl == MUSE_RESAMPLING_DISP_AWAV) {
            state = cpl_errorstate_get();
            muse_pixgrid *gridtest = muse_pixgrid_create(NULL, cube->header,
                                                         xsize, ysize, 5);
            cpl_test(!cpl_errorstate_is_equal(state) &&
                     cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
            cpl_errorstate_set(state);
            cpl_test_null(gridtest);
            gridtest = muse_pixgrid_create(pt, cube->header, 0, ysize, 5);
            cpl_test(!cpl_errorstate_is_equal(state) &&
                     cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
            cpl_errorstate_set(state);
            cpl_test_null(gridtest);
            gridtest = muse_pixgrid_create(pt, cube->header, xsize, 0, 5);
            cpl_test(!cpl_errorstate_is_equal(state) &&
                     cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
            cpl_errorstate_set(state);
            cpl_test_null(gridtest);
            muse_pixtable *pt33 = muse_pixtable_duplicate(pt);
            cpl_table_set_column_unit(pt33->table, MUSE_PIXTABLE_XPOS, "rad");
            cpl_table_set_column_unit(pt33->table, MUSE_PIXTABLE_YPOS, "rad");
            gridtest = muse_pixgrid_create(pt33, cube->header, xsize, ysize, 5);
            cpl_test(!cpl_errorstate_is_equal(state) &&
                     cpl_error_get_code() == CPL_ERROR_UNSUPPORTED_MODE);
            cpl_errorstate_set(state);
            cpl_test_null(gridtest);
            cpl_table_set_column_unit(pt33->table, MUSE_PIXTABLE_XPOS, "deg");
            cpl_table_set_column_unit(pt33->table, MUSE_PIXTABLE_YPOS, "deg");
            cpl_table_set_size(pt33->table, 0);
            gridtest = muse_pixgrid_create(pt33, cube->header, xsize, ysize, 5);
            cpl_test(!cpl_errorstate_is_equal(state) &&
                     cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
            cpl_errorstate_set(state);
            cpl_test_null(gridtest);
            muse_pixtable_delete(pt33);
            /* missing x/y columns cause the above "unsupported mode" error */
            pt33 = muse_pixtable_duplicate(pt);
            cpl_table_erase_column(pt33->table, MUSE_PIXTABLE_LAMBDA);
            gridtest = muse_pixgrid_create(pt33, cube->header, xsize, ysize, 5);
            cpl_test(!cpl_errorstate_is_equal(state) &&
                     cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND);
            cpl_errorstate_set(state);
            cpl_test_null(gridtest);
            muse_pixtable_delete(pt33);
            /* finally, create pixel tables that each have one extreme *
             * pixel that is missed in the grid construction; this is  *
             * constructed to fall beyond the -INT_MAX/2 limit...      */
            pt33 = muse_pixtable_duplicate(pt);
            cpl_table_set_float(pt33->table, MUSE_PIXTABLE_LAMBDA, 0, -1342171050.);
            gridtest = muse_pixgrid_create(pt33, cube->header, xsize, ysize, 5);
            cpl_test(!cpl_errorstate_is_equal(state) &&
                     cpl_error_get_code() == CPL_ERROR_ILLEGAL_OUTPUT);
            cpl_errorstate_set(state);
            cpl_test_nonnull(gridtest);
            muse_pixgrid_delete(gridtest);
            muse_pixtable_delete(pt33);
            pt33 = muse_pixtable_duplicate(pt);
            cpl_table_set_float(pt33->table, MUSE_PIXTABLE_LAMBDA, 0, 1342184800.);
            gridtest = muse_pixgrid_create(pt33, cube->header, xsize, ysize, 5);
            cpl_test(!cpl_errorstate_is_equal(state) &&
                     cpl_error_get_code() == CPL_ERROR_ILLEGAL_OUTPUT);
            cpl_errorstate_set(state);
            cpl_test_nonnull(gridtest);
            muse_pixgrid_delete(gridtest);
            muse_pixtable_delete(pt33);
          } /* if tl = awav */
        } /* for t = drizzle */
        cpl_free(fnw);
        /* primitive tests */
        cpl_test_nonnull(cube);
        cpl_test_nonnull(cube->data);
        cpl_test_nonnull(cube->header);
        int nplanes = cpl_imagelist_get_size(cube->data);
        if (tl == MUSE_RESAMPLING_DISP_AWAV || tl == MUSE_RESAMPLING_DISP_WAVE) {
          cpl_test(nplanes == 9); /* (6505 - 6494) / 1.25 = 8.8 planes */
        } else {
          /* for -LOG CTYPEs */
          cpl_test(nplanes == 14); /* (6505. - 6494.) / 1.25 * 1.6 = 14.08 planes */
        }
        /* if the extra extensions exist, they should have the same size */
        if (cube->dq) {
          cpl_test_eq(cpl_imagelist_get_size(cube->dq), nplanes);
        }
        if (cube->stat) {
          cpl_test_eq(cpl_imagelist_get_size(cube->stat), nplanes);
        }
        /* test direct grid collapsing */
        if (tl == MUSE_RESAMPLING_DISP_AWAV && t != MUSE_RESAMPLE_NEAREST) {
          cpl_test_nonnull(grid);
          cpl_errorstate state = cpl_errorstate_get();
          muse_resampling_params *p2 = muse_resampling_params_new(t);
          p2->tlambda = tl;
          muse_image *collapsed = muse_resampling_collapse_pixgrid(pt, grid, cube,
                                                                   fwhite, p2);
          cpl_test(cpl_errorstate_is_equal(state));
          cpl_test_nonnull(collapsed);
          cpl_test_nonnull(collapsed->data);
          cpl_test_nonnull(collapsed->stat);
          char *name2 = cpl_sprintf("grid-collapsed FOV (%s l%d %d)", unit, tl, t);
          muse_resampling_test_cubeimage_stats(collapsed->data, name2);
          cpl_free(name2);
          char *fnc = cpl_sprintf("cube_resampled_%s_l%d_%d_collapsed.fits", unit, tl, t);
          muse_image_save(collapsed, fnc);
#if MUSE_TEST_RESAMPLING_DELETE_FILES
          cpl_test(remove(fnc) == 0); /* delete the file again */
#endif
          cpl_free(fnc);
          muse_image_delete(collapsed);
          if (t == MUSE_RESAMPLE_WEIGHTED_DRIZZLE) {
            /* various failure cases of muse_resampling_collapse_pixgrid() */
            state = cpl_errorstate_get();
            collapsed = muse_resampling_collapse_pixgrid(NULL, grid, cube, fwhite, p2);
            cpl_test_null(collapsed);
            cpl_test(cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
            cpl_errorstate_set(state);
            collapsed = muse_resampling_collapse_pixgrid(pt, NULL, cube, fwhite, p2);
            cpl_test_null(collapsed);
            cpl_test(cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
            cpl_errorstate_set(state);
            collapsed = muse_resampling_collapse_pixgrid(pt, grid, NULL, fwhite, p2);
            cpl_test_null(collapsed);
            cpl_test(cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
            cpl_errorstate_set(state);
            collapsed = muse_resampling_collapse_pixgrid(pt, grid, cube, fwhite, NULL);
            cpl_test_null(collapsed);
            cpl_test(cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
            cpl_errorstate_set(state);
            /* this should succeed (same as white filter) */
            collapsed = muse_resampling_collapse_pixgrid(pt, grid, cube, NULL, p2);
            cpl_test_nonnull(collapsed);
            cpl_test(cpl_errorstate_is_equal(state));
            name2 = cpl_sprintf("grid-collapsed FOV nofilter (%s l%d %d)", unit, tl, t);
            muse_resampling_test_cubeimage_stats(collapsed->data, name2);
            cpl_free(name2);
            muse_image_delete(collapsed);
            /* this should fail again */
            p2->method = MUSE_RESAMPLE_NONE + 1;
            collapsed = muse_resampling_collapse_pixgrid(pt, grid, cube, fwhite, p2);
            cpl_test_null(collapsed);
            cpl_test(cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
            cpl_errorstate_set(state);
            p2->method = MUSE_RESAMPLE_NEAREST;
            collapsed = muse_resampling_collapse_pixgrid(pt, grid, cube, fwhite, p2);
            cpl_test_null(collapsed);
            cpl_test(cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
            cpl_errorstate_set(state);
          } /* if drizzle */
          muse_resampling_params_delete(p2);
          muse_pixgrid_delete(grid);
        } /* if AWAV (with output grid) */

        /* statistics on the central plane */
        unsigned int iplane = 5; /* at 6501.25 Angstrom */
        if (tl == MUSE_RESAMPLING_DISP_AWAV_LOG ||
            tl == MUSE_RESAMPLING_DISP_WAVE_LOG) {
          iplane = 8; /* also at 6501.25 Angstrom */
        }
        cpl_image *image = cpl_imagelist_get(cube->data, iplane);
        char *name = cpl_sprintf("FITS NAXIS=3 plane %d (%s l%d %d)",
                                 iplane + 1, unit, tl, t);
        if (t == MUSE_RESAMPLE_NEAREST && (tl == MUSE_RESAMPLING_DISP_AWAV_LOG ||
                                           tl == MUSE_RESAMPLING_DISP_WAVE_LOG)) {
          /* this plane will have far too many *
           * empty pixels, skip the test       */
        } else {
          muse_resampling_test_cubeimage_stats(image, name);
        }
        cpl_free(name);

        /* try to collapse the data using the white-light filter */
        muse_image *fov = muse_datacube_collapse(cube, fwhite);
        cpl_test_nonnull(fov);
        cpl_test_nonnull(fov->data);
        cpl_test_nonnull(fov->header);
        const char *fovunit = muse_pfits_get_bunit(fov->header);
        cpl_test_eq_string(fovunit, muse_pfits_get_bunit(cube->header));
        name = cpl_sprintf("FITS NAXIS=3 FOV (%s l%d %d)", unit, tl, t);
        muse_resampling_test_cubeimage_stats(fov->data, name);
        cpl_free(name);

        /* try to save the cube, first without collapsed image */
        char *fn = cpl_sprintf("cube_resampled_%s_l%d_%d.fits", unit, tl, t);
        cpl_test(muse_datacube_save(cube, fn) == CPL_ERROR_NONE);
        /* test for EXTNAME headers */
        cpl_propertylist *hdata = cpl_propertylist_load(fn, 1),
                         *hdq = cpl_propertylist_load(fn, 2),
                         *hstat = cpl_propertylist_load(fn, 3);
        cpl_test(!strcmp("DATA", cpl_propertylist_get_string(hdata, "EXTNAME")) &&
                 !strcmp("DQ", cpl_propertylist_get_string(hdq, "EXTNAME")) &&
                 !strcmp("STAT", cpl_propertylist_get_string(hstat, "EXTNAME")));
        /* test for ESO format headers produced by muse_image_save() */
        cpl_test(!strcmp("ESO", cpl_propertylist_get_string(hdata, "HDUCLASS")) &&
                 !strcmp("ESO", cpl_propertylist_get_string(hdq, "HDUCLASS")) &&
                 !strcmp("ESO", cpl_propertylist_get_string(hstat, "HDUCLASS")));
        cpl_test(!strcmp("DICD", cpl_propertylist_get_string(hdata, "HDUDOC")) &&
                 !strcmp("DICD", cpl_propertylist_get_string(hdq, "HDUDOC")) &&
                 !strcmp("DICD", cpl_propertylist_get_string(hstat, "HDUDOC")));
        cpl_test(!strncmp("DICD version ", cpl_propertylist_get_string(hdata, "HDUVERS"), 13) &&
                 !strncmp("DICD version ", cpl_propertylist_get_string(hdq, "HDUVERS"), 13) &&
                 !strncmp("DICD version ", cpl_propertylist_get_string(hstat, "HDUVERS"), 13));
        cpl_test(!strcmp("IMAGE", cpl_propertylist_get_string(hdata, "HDUCLAS1")) &&
                 !strcmp("IMAGE", cpl_propertylist_get_string(hdq, "HDUCLAS1")) &&
                 !strcmp("IMAGE", cpl_propertylist_get_string(hstat, "HDUCLAS1")));
        cpl_test(!strcmp("DATA", cpl_propertylist_get_string(hdata, "HDUCLAS2")) &&
                 !strcmp("QUALITY", cpl_propertylist_get_string(hdq, "HDUCLAS2")) &&
                 !strcmp("ERROR", cpl_propertylist_get_string(hstat, "HDUCLAS2")));
        cpl_test(!cpl_propertylist_has(hdata, "HDUCLAS3") && /* shouldn't exist */
                 !strcmp("FLAG32BIT", cpl_propertylist_get_string(hdq, "HDUCLAS3")) &&
                 !strcmp("MSE", cpl_propertylist_get_string(hstat, "HDUCLAS3")));
        cpl_test(!strcmp("STAT", cpl_propertylist_get_string(hdata, "ERRDATA")) &&
                 !strcmp("DQ", cpl_propertylist_get_string(hdata, "QUALDATA")) &&
                 !cpl_propertylist_has(hdata, "SCIDATA"));
        cpl_test(!strcmp("DATA", cpl_propertylist_get_string(hstat, "SCIDATA")) &&
                 !strcmp("DQ", cpl_propertylist_get_string(hstat, "QUALDATA")) &&
                 !cpl_propertylist_has(hstat, "ERRDATA"));
        cpl_test(!strcmp("DATA", cpl_propertylist_get_string(hdq, "SCIDATA")) &&
                 !strcmp("STAT", cpl_propertylist_get_string(hdq, "ERRDATA")) &&
                 !cpl_propertylist_has(hdq, "QUALDATA"));
        cpl_test(cpl_propertylist_has(hdq, "QUALMASK"));
        cpl_propertylist_delete(hdata);
        cpl_propertylist_delete(hdq);
        cpl_propertylist_delete(hstat);
#if MUSE_TEST_RESAMPLING_DELETE_FILES
        cpl_test(remove(fn) == 0); /* delete the file again */
#endif
        cpl_free(fn);
        /* now add reconstructed image to cube and try to save again */
        cube->recimages = muse_imagelist_new();
        muse_imagelist_set(cube->recimages, fov, 0);
        cube->recnames = cpl_array_new(1, CPL_TYPE_STRING);
        cpl_array_set_string(cube->recnames, 0, "white");
        fn = cpl_sprintf("cube_resampled_%s_l%d_%d_with_recimage.fits", unit, tl, t);
        cpl_test(muse_datacube_save(cube, fn) == CPL_ERROR_NONE);
        /* test the the headers now also worked for the reconstructed image */
        cpl_propertylist *him = cpl_propertylist_load(fn, 4),
                         *himdq = cpl_propertylist_load(fn, 5);
        cpl_test_eq_string(fovunit, muse_pfits_get_bunit(him));
        cpl_test_zero(cpl_propertylist_has(himdq, "BUNIT")); /* no BUNIT stored for DQ! */
        cpl_test(!strcmp("white", cpl_propertylist_get_string(him, "EXTNAME")) &&
                 !strcmp("white_DQ", cpl_propertylist_get_string(himdq, "EXTNAME")));
        /* test for ESO format headers produced by muse_image_save() */
        cpl_test(!strcmp("ESO", cpl_propertylist_get_string(him, "HDUCLASS")) &&
                 !strcmp("ESO", cpl_propertylist_get_string(himdq, "HDUCLASS")));
        cpl_test(!strcmp("DICD", cpl_propertylist_get_string(him, "HDUDOC")) &&
                 !strcmp("DICD", cpl_propertylist_get_string(himdq, "HDUDOC")));
        cpl_test(!strncmp("DICD version ", cpl_propertylist_get_string(him, "HDUVERS"), 13) &&
                 !strncmp("DICD version ", cpl_propertylist_get_string(himdq, "HDUVERS"), 13));
        cpl_test(!strcmp("IMAGE", cpl_propertylist_get_string(him, "HDUCLAS1")) &&
                 !strcmp("IMAGE", cpl_propertylist_get_string(himdq, "HDUCLAS1")));
        cpl_test(!strcmp("DATA", cpl_propertylist_get_string(him, "HDUCLAS2")) &&
                 !strcmp("QUALITY", cpl_propertylist_get_string(himdq, "HDUCLAS2")));
        cpl_test(!cpl_propertylist_has(him, "HDUCLAS3") && /* shouldn't exist */
                 !strcmp("FLAG32BIT", cpl_propertylist_get_string(himdq, "HDUCLAS3")));
        cpl_test(!cpl_propertylist_has(him, "ERRDATA") &&
                 !strcmp("white_DQ", cpl_propertylist_get_string(him, "QUALDATA")) &&
                 !cpl_propertylist_has(him, "SCIDATA"));
        cpl_test(!cpl_propertylist_has(himdq, "QUALDATA") &&
                 !strcmp("white", cpl_propertylist_get_string(himdq, "SCIDATA")) &&
                 !cpl_propertylist_has(himdq, "ERRDATA") &&
                 cpl_propertylist_has(himdq, "QUALMASK"));
        cpl_propertylist_delete(him);
        cpl_propertylist_delete(himdq);
#if MUSE_TEST_RESAMPLING_DELETE_FILES
        cpl_test(remove(fn) == 0); /* delete the file again */
#endif
        cpl_free(fn);
        cpl_errorstate prestate = cpl_errorstate_get();
        muse_resampling_params_delete(params);
        cpl_test(cpl_errorstate_is_equal(prestate));

        /* ====================================================================== *
         * Second test is almost the same but for the Euro3D format               *
         * ====================================================================== */
        cpl_msg_info(__func__, "Resampling pixel table [%s] to Euro3D, method %d",
                     unit, t);
        params = muse_resampling_params_new(t);
        params->tlambda = tl;
        if (t == MUSE_RESAMPLE_WEIGHTED_LANCZOS) {
          params->ld = 3; /* want to test lanczos3 */
        }
        muse_euro3dcube *e3d = muse_resampling_euro3d(pt, params);
        /* primitive tests */
        cpl_test_nonnull(e3d);
        cpl_test_nonnull(e3d->dtable);
        cpl_test_nonnull(e3d->hdata);
        cpl_test_nonnull(e3d->gtable);
        cpl_test_nonnull(e3d->header);
        int nwl = cpl_table_get_column_depth(e3d->dtable, "DATA_SPE");
        cpl_test(nwl == nplanes); /* same number as planes in cube above */
        if (cpl_table_has_column(e3d->dtable, "QUAL_SPE")) {
          cpl_test_eq(cpl_table_get_column_depth(e3d->dtable, "DATA_SPE"), nwl);
        }
        if (cpl_table_has_column(e3d->dtable, "STAT_SPE")) {
          cpl_test_eq(cpl_table_get_column_depth(e3d->dtable, "STAT_SPE"), nwl);
        }

        /* try to collapse the data using the white-light filter */
        muse_image *fove3d = muse_euro3dcube_collapse(e3d, fwhite);
        cpl_test_nonnull(fove3d);
        cpl_test_nonnull(fove3d->data);
        name = cpl_sprintf("Euro3D FOV (%s l%d %d)", unit, tl, t);
        muse_resampling_test_cubeimage_stats(fove3d->data, name);
        cpl_free(name);

        /* try to save the Euro3D object, first without collapsed image */
        fn = cpl_sprintf("cube_resampled_%s_l%d_%d_e3d.fits", unit, tl, t);
        cpl_test(muse_euro3dcube_save(e3d, fn) == CPL_ERROR_NONE);
#if MUSE_TEST_RESAMPLING_DELETE_FILES
        cpl_test(remove(fn) == 0); /* delete the file again */
#endif
        cpl_free(fn);
        /* now add reconstructed image to Euro3D object and try to save again */
        e3d->recimages = muse_imagelist_new();
        muse_imagelist_set(e3d->recimages, fove3d, 0);
        e3d->recnames = cpl_array_new(1, CPL_TYPE_STRING);
        cpl_array_set_string(e3d->recnames, 0, "white");
        fn = cpl_sprintf("cube_resampled_%s_l%d_%d_e3d_with_recimage.fits", unit, tl, t);
        cpl_test(muse_euro3dcube_save(e3d, fn) == CPL_ERROR_NONE);
#if MUSE_TEST_RESAMPLING_DELETE_FILES
        cpl_test(remove(fn) == 0); /* delete the file again */
#endif
        cpl_free(fn);

        /* test that both collapsed images are idential */
        cpl_test_image_abs(fov->data, fove3d->data, 0.001 /* absolute tolerance! */);

        prestate = cpl_errorstate_get();
        if (t == 0 && ipt == 0) {
          /* for the first type, test failure cases of the collapsing functions */
          muse_image *fovnew = muse_datacube_collapse(NULL, fwhite);
          cpl_test_null(fovnew);
          cpl_errorstate_set(prestate);
          fovnew = muse_euro3dcube_collapse(NULL, fwhite);
          cpl_test_null(fovnew);
          cpl_errorstate_set(prestate);

          /* no filter means to collapse everything, this should be identical *
           * to the white-light filter for this wavelength-limited test data  */
          fovnew = muse_datacube_collapse(cube, NULL);
          cpl_test_image_abs(fov->data, fovnew->data, 0.001 /* absolute tolerance! */);
          muse_image_delete(fovnew);
          fovnew = muse_euro3dcube_collapse(e3d, NULL);
          cpl_test_image_abs(fov->data, fovnew->data, 0.001 /* absolute tolerance! */);
          muse_image_delete(fovnew);
        } /* if first loop */

        /* some further tests with cubes that only need to be run once or so... */
        if (t == MUSE_RESAMPLE_WEIGHTED_DRIZZLE) {
          /* use this cube's WCS to try the OUTPUT_WCS-like  *
           * method to position the resampled cube somewhere */
          cpl_propertylist *hwcs = cpl_propertylist_duplicate(cube->header);
          cpl_propertylist_update_int(hwcs, "NAXIS3", 4); /* force dispersion axis */
          cpl_wcs *cwcs = cpl_wcs_new_from_propertylist(hwcs);
          cpl_propertylist_delete(hwcs);
          muse_resampling_params *pwcs = muse_resampling_params_new(t);
          pwcs->tlambda = tl;
          pwcs->wcs = cwcs;
          cpl_errorstate state = cpl_errorstate_get();
          muse_datacube *cubepos = muse_resampling_cube(pt, pwcs, NULL);
          muse_resampling_params_delete(pwcs);
          cpl_test(cpl_errorstate_is_equal(state));
          cpl_test_nonnull(cubepos);
          /* check that the forced size got set right */
          cpl_test(cpl_imagelist_get_size(cubepos->data) == 4);
          /* the 2nd plane should be the same for this and the non-forced cube */
          cpl_test_image_abs(cpl_imagelist_get(cubepos->data, 1),
                             cpl_imagelist_get(cube->data, 1), FLT_EPSILON);
          cpl_test_image_abs(cpl_imagelist_get(cubepos->dq, 1),
                             cpl_imagelist_get(cube->dq, 1), 0);
          cpl_test_image_abs(cpl_imagelist_get(cubepos->stat, 1),
                             cpl_imagelist_get(cube->stat, 1), FLT_EPSILON);
          char *fn2 = cpl_sprintf("cube_resampled_%s_l%d_%d_naxis3_overridden.fits",
                            unit, tl, t);
          cpl_test(muse_datacube_save(cubepos, fn2) == CPL_ERROR_NONE);
          muse_datacube_delete(cubepos);
#if MUSE_TEST_RESAMPLING_DELETE_FILES
          cpl_test(remove(fn2) == 0); /* delete the file again */
#endif
          cpl_free(fn2);

          /* now create two cubes, which align nicely in wavelength, *
           * using OUTPUT_WCS-like setups                            */
          hwcs = cpl_propertylist_duplicate(cube->header);
          cpl_propertylist_update_int(hwcs, "NAXIS3", 4); /* force dispersion axis */
          cpl_wcs *cwcs1 = cpl_wcs_new_from_propertylist(hwcs);
          double crval3 = muse_pfits_get_crval(hwcs, 3),
                 cd3_3 = muse_pfits_get_cd(hwcs, 3, 3);
          cpl_propertylist_update_double(hwcs, "CRVAL3", crval3 + 4 * cd3_3);
          cpl_wcs *cwcs2 = cpl_wcs_new_from_propertylist(hwcs);
          cpl_propertylist_delete(hwcs);
          muse_resampling_params *pwcs1 = muse_resampling_params_new(t),
                                 *pwcs2 = muse_resampling_params_new(t);
          pwcs1->tlambda = tl;
          pwcs1->wcs = cwcs1;
          printf("CRVAL (1):\n");
          cpl_array_dump(cpl_wcs_get_crval(cwcs1), 0, 12, stdout);
          printf("CRPIX (1):\n");
          cpl_array_dump(cpl_wcs_get_crpix(cwcs1), 0, 12, stdout);
          printf("CD (1):\n");
          cpl_matrix_dump(cpl_wcs_get_cd(cwcs1), stdout);
          fflush(stdout);
          pwcs2->tlambda = tl;
          pwcs2->wcs = cwcs2;
          printf("CRVAL (2):\n");
          cpl_array_dump(cpl_wcs_get_crval(cwcs2), 0, 12, stdout);
          printf("CRPIX (2):\n");
          cpl_array_dump(cpl_wcs_get_crpix(cwcs2), 0, 12, stdout);
          printf("CD (2):\n");
          cpl_matrix_dump(cpl_wcs_get_cd(cwcs2), stdout);
          fflush(stdout);
          muse_datacube *cube1 = muse_resampling_cube(pt, pwcs1, NULL),
                        *cube2 = muse_resampling_cube(pt, pwcs2, NULL);
          muse_resampling_params_delete(pwcs1);
          muse_resampling_params_delete(pwcs2);
          cpl_test_nonnull(cube1);
          cpl_test_nonnull(cube2);
          cpl_test_eq(cpl_imagelist_get_size(cube1->data), 4);
          cpl_test_eq(cpl_imagelist_get_size(cube2->data), 4);
          if (tl == MUSE_RESAMPLING_DISP_AWAV ||
              tl == MUSE_RESAMPLING_DISP_WAVE) {
            /* concat should go without problems and create cube twice the length */
            cpl_test(muse_datacube_concat(cube1, cube2) == CPL_ERROR_NONE);
            cpl_test_eq(cpl_imagelist_get_size(cube1->data), 8);

            /* doing it again, should create problems, *
             * since the cubes do not align any more   */
            prestate = cpl_errorstate_get();
            cpl_test(muse_datacube_concat(cube1, cube2) == CPL_ERROR_ILLEGAL_INPUT);
            cpl_test(!cpl_errorstate_is_equal(prestate));
            cpl_test_eq(cpl_imagelist_get_size(cube1->data), 8); /* still same size */
            cpl_errorstate_set(prestate);
            /* NULL arguments should also cause failures */
            cpl_test(muse_datacube_concat(cube1, NULL) == CPL_ERROR_NULL_INPUT);
            cpl_test(!cpl_errorstate_is_equal(prestate));
            cpl_errorstate_set(prestate);
            prestate = cpl_errorstate_get();
            cpl_test(muse_datacube_concat(NULL, cube2) == CPL_ERROR_NULL_INPUT);
            cpl_test(!cpl_errorstate_is_equal(prestate));
            cpl_errorstate_set(prestate);
          } else if (tl == MUSE_RESAMPLING_DISP_AWAV_LOG ||
                     tl == MUSE_RESAMPLING_DISP_WAVE_LOG) {
            /* concat should fail for log axes  */
            prestate = cpl_errorstate_get();
            cpl_test(muse_datacube_concat(cube1, cube2) == CPL_ERROR_UNSUPPORTED_MODE);
            cpl_test(!cpl_errorstate_is_equal(prestate));
            cpl_errorstate_set(prestate);
          }
          muse_datacube_delete(cube1);
          muse_datacube_delete(cube2);

          /* also try to convert the DQ to NANs and save again */
          fn2 = cpl_sprintf("cube_resampled_%s_l%d_%d_with_recimage_NANs.fits",
                            unit, tl, t);
          state = cpl_errorstate_get();
          cpl_test(muse_datacube_convert_dq(cube) == CPL_ERROR_NONE);
          cpl_test_null(cube->dq);
          /* CPL statistics should now derive NAN, at least where  *
           * NANs are present, i.e. in the first plane of the cube */
          cpl_test(isnan(cpl_image_get_mean(cpl_imagelist_get(cube->data, 0))));
          cpl_test(isnan(cpl_image_get_mean(cpl_imagelist_get(cube->stat, 0))));
          cpl_test(muse_datacube_save(cube, fn2) == CPL_ERROR_NONE);
          cpl_test(cpl_errorstate_is_equal(state));
          /* count extensions, then save the reconstructed images and count again */
          int next = cpl_fits_count_extensions(fn2);
#if 0
          char *sys = cpl_sprintf("dfits -x 0 %s | grep EXTNAME", fn2);
          system(sys);
#endif
          cpl_test(muse_datacube_save_recimages(fn2, cube->recimages, cube->recnames)
                   == CPL_ERROR_NONE);
          int next2 = cpl_fits_count_extensions(fn2);
#if 0
          cpl_msg_debug("blablabla", "%d -> %d", next, next2);
          system(sys);
          cpl_free(sys);
#endif
          cpl_test_eq(next2, next + 1); /* one more extension now */
          muse_datacube *cubel = muse_datacube_load(fn2);
          cpl_test_nonnull(cubel);
          cpl_test_nonnull(cubel->data);
          cpl_test_nonnull(cubel->stat);
          cpl_test_eq(cpl_array_get_size(cubel->recnames), 2);
          cpl_test_eq(muse_imagelist_get_size(cubel->recimages), 2);
          muse_datacube_delete(cubel);
#if MUSE_TEST_RESAMPLING_DELETE_FILES
          cpl_test(remove(fn2) == 0); /* delete the file again */
#endif
          cpl_free(fn2);

          /* test failure cases */
          state = cpl_errorstate_get();
          /* converting again should fail because the dq component is now missing */
          cpl_test(muse_datacube_convert_dq(cube) == CPL_ERROR_NULL_INPUT);
          cpl_errorstate_set(state);
          /* NULL input should fail, too */
          cpl_test(muse_datacube_convert_dq(NULL) == CPL_ERROR_NULL_INPUT);
          cpl_errorstate_set(state);
        } /* if drizzle */

        /* for one of the tests, also create a mask cube and test the bad pixel table creation this way */
        if (t == MUSE_RESAMPLE_WEIGHTED_DRIZZLE &&
            tl == MUSE_RESAMPLING_DISP_AWAV && ipt == 0) {
          setenv("MUSE_BADPIX_FROM_MASKCUBE",
                 "maskcube_test.fits,badpix_test.fits", 1);
          /* create the mask cube */
          int nx = cpl_image_get_size_x(cpl_imagelist_get(cube->data, 0)),
              ny = cpl_image_get_size_y(cpl_imagelist_get(cube->data, 0));
          cpl_image *mask = cpl_image_new(nx, ny, CPL_TYPE_INT);
          cpl_image_fill_window(mask, 8, 3, 12, 5, 1);
          cpl_imagelist *maskcube = cpl_imagelist_new();
          int i, err;
          for (i = 0; i < nplanes; i++) {
            cpl_imagelist_set(maskcube, mask, i);
          } /* for i (all planes) */
          /* different mask for the next-to-last plane */
          cpl_image *mask2 = cpl_image_new(nx, ny, CPL_TYPE_INT);
          cpl_image_fill_window(mask2, 10, 5, 30, 9, 1);
          cpl_imagelist_set(maskcube, mask2, nplanes - 2);
          /* and save the masking cube */
          cpl_imagelist_save(maskcube, "maskcube_test.fits", CPL_TYPE_INT, NULL,
                             CPL_IO_CREATE);
          cpl_imagelist_delete(maskcube);
          cpl_test_zero(access("maskcube_test.fits", F_OK));
          /* now do the resampling with bad pixel mask creation */
          muse_resampling_params *parnocr = muse_resampling_params_new(t);
          parnocr->tlambda = tl;
          parnocr->crsigma = -1.;
          muse_datacube *cb = muse_resampling_cube(pt, parnocr, NULL);
          muse_resampling_params_delete(parnocr);
          cpl_test(isnan(cpl_image_get(cpl_imagelist_get(cb->stat, 3), 8, 3, &err)));
          cpl_test(isnan(cpl_image_get(cpl_imagelist_get(cb->stat, 3), 12, 5, &err)));
          cpl_test(!isnan(cpl_image_get(cpl_imagelist_get(cb->stat, 3), 7, 2, &err)));
          cpl_test(!isnan(cpl_image_get(cpl_imagelist_get(cb->stat, 3), 13, 6, &err)));
          cpl_test(isnan(cpl_image_get(cpl_imagelist_get(cb->stat, nplanes - 2), 10, 5, &err)));
          cpl_test(isnan(cpl_image_get(cpl_imagelist_get(cb->stat, nplanes - 2), 30, 9, &err)));
          muse_datacube_save(cb, "test_cube.fits");
          muse_datacube_delete(cb);
          cpl_test_zero(access("badpix_test.fits", F_OK));
#if MUSE_TEST_RESAMPLING_DELETE_FILES
          cpl_test(remove("maskcube_test.fits") == 0);
          cpl_test(remove("test_cube.fits") == 0);
          cpl_test(remove("badpix_test.fits") == 0);
#endif
          cpl_test(unsetenv("MUSE_BADPIX_FROM_MASKCUBE") == 0);
        }

        /* now we are done with both cubes and the parameters */
        prestate = cpl_errorstate_get();
        muse_datacube_delete(cube);
        cpl_test(cpl_errorstate_is_equal(prestate));
        prestate = cpl_errorstate_get();
        muse_euro3dcube_delete(e3d);
        cpl_test(cpl_errorstate_is_equal(prestate));

        prestate = cpl_errorstate_get();
        muse_resampling_params_delete(params);
        cpl_test(cpl_errorstate_is_equal(prestate));
      } /* for ipt (both pixel tables) */
    } /* for t (all cube resampling methods) */
  } /* for tl (all wavelength dispersion types) */
  muse_pixtable_delete(ptdeg);

  /* test failure and special cases */
  muse_resampling_params *params =
    muse_resampling_params_new(MUSE_RESAMPLE_NEAREST);
  /* for FITS NAXIS=3 */
  cpl_errorstate ps = cpl_errorstate_get();
  muse_datacube *cube = muse_resampling_cube(NULL, params, NULL);
  cpl_test_null(cube);
  cpl_errorstate_set(ps);
  cube = muse_resampling_cube(aPtPix, NULL, NULL);
  cpl_test_null(cube);
  cpl_errorstate_set(ps);
  params->method = MUSE_RESAMPLE_NONE; /* special case of no resampling */
  muse_pixgrid *grid;
  cube = muse_resampling_cube(aPtPix, params, &grid);
  cpl_test_nonnull(grid);
  muse_pixgrid_delete(grid);
  cpl_test_nonnull(cube);
  cpl_test_nonnull(cube->header);
  muse_datacube_delete(cube);
  params->method = MUSE_RESAMPLE_NONE + 1; /* invalid type */
  ps = cpl_errorstate_get();
  cube = muse_resampling_cube(aPtPix, params, NULL);
  cpl_test_null(cube);
  cpl_errorstate_set(ps);
  /* for Euro3D */
  params->method = MUSE_RESAMPLE_NEAREST;
  muse_euro3dcube *e3d = muse_resampling_euro3d(NULL, params);
  cpl_test_null(e3d);
  cpl_errorstate_set(ps);
  e3d = muse_resampling_euro3d(aPtPix, NULL);
  cpl_test_null(e3d);
  cpl_errorstate_set(ps);
  params->method = MUSE_RESAMPLE_NONE; /* no resampling invalid for Euro3D */
  e3d = muse_resampling_euro3d(aPtPix, params);
  cpl_test_null(e3d);
  cpl_errorstate_set(ps);
  params->method = MUSE_RESAMPLE_NONE; /* no resampling invalid for Euro3D */
  muse_resampling_params_delete(params);
  /* test parameter structure creation */
  ps = cpl_errorstate_get();
  params = muse_resampling_params_new(MUSE_RESAMPLE_NONE + 1);
  cpl_test_null(params);
  cpl_errorstate_set(ps);
  params = muse_resampling_params_new(MUSE_RESAMPLE_NEAREST - 1);
  cpl_test_null(params);
  cpl_errorstate_set(ps);
  /* test parameter wcs setting */
  params = muse_resampling_params_new(MUSE_RESAMPLE_WEIGHTED_DRIZZLE);
  params->tlambda = MUSE_RESAMPLING_DISP_AWAV_LOG;
  ps = cpl_errorstate_get();
  muse_resampling_params_set_wcs(params, NULL);
  cpl_test_null(params->wcs);
  cpl_test(params->tlambda == MUSE_RESAMPLING_DISP_AWAV);
  cpl_test(cpl_errorstate_is_equal(ps));
  cpl_propertylist *header = cpl_propertylist_new();
  ps = cpl_errorstate_get();
  muse_resampling_params_set_wcs(params, header);
  cpl_test_null(params->wcs); /* empty header fails cpl_wcs_new_from_propertylist() */
  cpl_test(params->tlambda == MUSE_RESAMPLING_DISP_AWAV);
  cpl_test(!cpl_errorstate_is_equal(ps));
  cpl_errorstate_set(ps);
  /* cpl_wcs_new_from_propertylist() tests for WCS presence by checking CRVAL... */
  cpl_propertylist_append_double(header, "CRVAL1", 1.);
  cpl_propertylist_append_double(header, "CRVAL2", 2.);
  cpl_propertylist_append_double(header, "CRVAL3", 3.);
  cpl_propertylist_append_string(header, "CTYPE3", "AWAV-LOG");
  /* it also throws an error if both CDi_j and PCi_j are missing (for CPL 6.4) */
  cpl_propertylist_append_double(header, "PC1_1", -0.2 / 3600.);
  cpl_propertylist_append_double(header, "PC2_2",  0.2 / 3600.);
  muse_resampling_params_set_wcs(params, header);
  cpl_test_nonnull(params->wcs);
  cpl_test(params->tlambda == MUSE_RESAMPLING_DISP_AWAV_LOG);
  cpl_test(cpl_errorstate_is_equal(ps)); /* cpl_wcs_new_from_propertylist() outputs warning! */
  cpl_propertylist_update_string(header, "CTYPE3", "WAVE");
  cpl_wcs_delete(params->wcs);
  params->wcs = NULL;
  muse_resampling_params_set_wcs(params, header);
  cpl_test_nonnull(params->wcs);
  cpl_test(params->tlambda == MUSE_RESAMPLING_DISP_WAVE);
  cpl_propertylist_update_string(header, "CTYPE3", "WAVE-LOG");
  cpl_wcs_delete(params->wcs);
  params->wcs = NULL;
  muse_resampling_params_set_wcs(params, header);
  cpl_test_nonnull(params->wcs);
  cpl_test(params->tlambda == MUSE_RESAMPLING_DISP_WAVE_LOG);
  cpl_propertylist_update_string(header, "CTYPE3", "AWAV-LOG");
  cpl_wcs_delete(params->wcs);
  params->wcs = NULL;
  muse_resampling_params_set_wcs(params, header);
  cpl_test_nonnull(params->wcs);
  cpl_test(params->tlambda == MUSE_RESAMPLING_DISP_AWAV_LOG);
  cpl_propertylist_delete(header);
  muse_resampling_params_delete(params);
  /* parameter deletion is also supposed to work with NULL argument */
  ps = cpl_errorstate_get();
  muse_resampling_params_delete(NULL);
  cpl_test(cpl_errorstate_is_equal(ps));
  /* test loading non-existing cube */
  ps = cpl_errorstate_get();
  cube = muse_datacube_load("blabla.fits");
  cpl_test_null(cube);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  /* test loading cube from file that does not have a DATA extension */
  cpl_image *imbla = cpl_image_new(3, 3, CPL_TYPE_FLOAT);
  cpl_image_save(imbla, "blabla.fits", CPL_TYPE_UNSPECIFIED, NULL, CPL_IO_CREATE);
  cpl_image_delete(imbla);
  ps = cpl_errorstate_get();
  cube = muse_datacube_load("blabla.fits");
  cpl_test_null(cube);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(ps);
  remove("blabla.fits");

  muse_table_delete(fwhite);
  return CPL_ERROR_NONE;
} /* muse_test_resampling_cube() */

static cpl_error_code
muse_test_resampling_spectrum(muse_pixtable *aPtPix)
{
  cpl_errorstate state = cpl_errorstate_get();
  cpl_table *spec1 = muse_resampling_spectrum(aPtPix, 1.25),
            *spec2 = muse_resampling_spectrum(aPtPix, 0.3125);
  cpl_test(cpl_errorstate_is_equal(state));
#if 0
  cpl_table_save(spec1, NULL, NULL, "spec1.fits", CPL_IO_CREATE);
  cpl_table_save(spec2, NULL, NULL, "spec2.fits", CPL_IO_CREATE);
#endif
  /* the ranges are the results of the testing pixel table */
  cpl_test_eq(cpl_table_get_nrow(spec1), 9);
  cpl_test_eq(cpl_table_get_nrow(spec2), 33);
  cpl_test_abs(cpl_table_get_column_mean(spec1, "lambda"), 6500., DBL_EPSILON);
  cpl_test_abs(cpl_table_get_column_min(spec1, "lambda"), 6495., DBL_EPSILON);
  cpl_test_abs(cpl_table_get_column_max(spec1, "lambda"), 6505., DBL_EPSILON);
  cpl_test_abs(cpl_table_get_column_mean(spec2, "lambda"), 6500., DBL_EPSILON);
  cpl_test_abs(cpl_table_get_column_min(spec2, "lambda"), 6495., DBL_EPSILON);
  cpl_test_abs(cpl_table_get_column_max(spec2, "lambda"), 6505., DBL_EPSILON);
  cpl_test_eq(cpl_table_get_column_min(spec1, "dq"), 0);
  cpl_test_eq(cpl_table_get_column_max(spec1, "dq"), 0);
  cpl_test_eq(cpl_table_get_column_min(spec2, "dq"), 0);
  cpl_test_eq(cpl_table_get_column_max(spec2, "dq"), 0);
  /* the resulting statistics differ a little bit */
  cpl_test_abs(cpl_table_get_column_mean(spec1, "data"), 4.4262, 1.1e-5);
  cpl_test_abs(cpl_table_get_column_stdev(spec1, "data"), 1.6889, 1.1e-5);
  cpl_test_abs(cpl_table_get_column_mean(spec2, "data"), 4.5828, 1.0e-5);
  cpl_test_abs(cpl_table_get_column_stdev(spec2, "data"), 1.8941, 1.2e-5);
  /* also somewhat close to the original mean of the input pixel table */
  cpl_test_abs(cpl_table_get_column_mean(spec1, "data"), 4.5693, 0.15);
  cpl_test_abs(cpl_table_get_column_mean(spec2, "data"), 4.5693, 0.015);

  /* with with moderate rejection, i.e. muse_resampling_spectrum_iterate() */
  state = cpl_errorstate_get();
  cpl_table *spec3 = muse_resampling_spectrum_iterate(aPtPix, 1.25, 15., 15., 1),
            *spec4 = muse_resampling_spectrum_iterate(aPtPix, 0.3125, 15., 15., 3);
  cpl_test(cpl_errorstate_is_equal(state));
#if 0
  cpl_table_save(spec3, NULL, NULL, "spec3.fits", CPL_IO_CREATE);
  cpl_table_save(spec4, NULL, NULL, "spec4.fits", CPL_IO_CREATE);
#endif
  /* the ranges are the results of the testing pixel table, are unchanged *
   * (i.e. not bin was rejected completely)                               */
  cpl_test_eq(cpl_table_get_nrow(spec3), 9);
  cpl_test_eq(cpl_table_get_nrow(spec4), 33);
  cpl_test_abs(cpl_table_get_column_mean(spec3, "lambda"), 6500., DBL_EPSILON);
  cpl_test_abs(cpl_table_get_column_min(spec3, "lambda"), 6495., DBL_EPSILON);
  cpl_test_abs(cpl_table_get_column_max(spec3, "lambda"), 6505., DBL_EPSILON);
  cpl_test_abs(cpl_table_get_column_mean(spec4, "lambda"), 6500., DBL_EPSILON);
  cpl_test_abs(cpl_table_get_column_min(spec4, "lambda"), 6495., DBL_EPSILON);
  cpl_test_abs(cpl_table_get_column_max(spec4, "lambda"), 6505., DBL_EPSILON);
  cpl_test_eq(cpl_table_get_column_min(spec3, "dq"), 0);
  cpl_test_eq(cpl_table_get_column_min(spec3, "dq"), 0);
  cpl_test_eq(cpl_table_get_column_max(spec4, "dq"), 0);
  cpl_test_eq(cpl_table_get_column_max(spec4, "dq"), 0);
  /* the resulting statistics with rejection differ a little bit */
  cpl_test_abs(cpl_table_get_column_mean(spec3, "data"), 4.1687, 1.1e-5);
  cpl_test_abs(cpl_table_get_column_stdev(spec3, "data"), 1.6700, 3.6e-5);
  cpl_test_abs(cpl_table_get_column_mean(spec4, "data"), 4.3005, 1.8e-5);
  cpl_test_abs(cpl_table_get_column_stdev(spec4, "data"), 1.8200, 2.7e-5);
  /* tests against the original pixel table are meaningless now; *
   * compare with the unrejected table instead                   */
  cpl_table_duplicate_column(spec1, "data3", spec3, "data");
  cpl_table_duplicate_column(spec2, "data4", spec4, "data");
  cpl_table_delete(spec4);
  cpl_table_delete(spec3);
  cpl_table_subtract_columns(spec1, "data", "data3");
  cpl_table_subtract_columns(spec2, "data", "data4");
#if 0
  cpl_table_save(spec1, NULL, NULL, "spec1sub.fits", CPL_IO_CREATE);
  cpl_table_save(spec2, NULL, NULL, "spec2sub.fits", CPL_IO_CREATE);
#endif
  /* the new ones got only high values rejected so should be lower or equal *
   * everywhere                                                             */
  cpl_test(cpl_table_get_column_min(spec1, "data") >= 0.);
  cpl_test(cpl_table_get_column_min(spec2, "data") >= 0.);
  cpl_test_abs(cpl_table_get_column_mean(spec1, "data"), 0.2575, 1.0e-5);
  cpl_test_abs(cpl_table_get_column_stdev(spec1, "data"), 0.0694, 3.3e-5);
  cpl_test_abs(cpl_table_get_column_mean(spec2, "data"), 0.2823, 2.6e-5);
  cpl_test_abs(cpl_table_get_column_stdev(spec2, "data"), 0.2753, 1.4e-5);
  cpl_table_delete(spec2);
  cpl_table_delete(spec1);

  /* failure cases */
  state = cpl_errorstate_get();
  /* NULL input */
  spec1 = muse_resampling_spectrum(NULL, 1.25);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  spec3 = muse_resampling_spectrum_iterate(NULL, 1.25, 15., 15., 1);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  /* try invalid pixel table as input, with different missing columns */
  const char *cols[] = { MUSE_PIXTABLE_LAMBDA, MUSE_PIXTABLE_DATA,
                         MUSE_PIXTABLE_DQ, MUSE_PIXTABLE_STAT, NULL };
  int i;
  for (i = 0; cols[i]; i++) {
    /* rename this column to a bad name, so that it's missing */
    char *badcol = cpl_sprintf("%s_bad", cols[i]);
    cpl_table_name_column(aPtPix->table, cols[i], badcol);
    cpl_msg_debug(__func__, "%s -> %s", cols[i], badcol);

    state = cpl_errorstate_get();
    spec1 = muse_resampling_spectrum(aPtPix, 1.25);
    cpl_test(!cpl_errorstate_is_equal(state) &&
             cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
    cpl_errorstate_set(state);
    spec3 = muse_resampling_spectrum_iterate(aPtPix, 1.25, 15., 15., 1);
    cpl_test(!cpl_errorstate_is_equal(state) &&
             cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
    cpl_errorstate_set(state);

    /* reset the name to the original */
    cpl_table_name_column(aPtPix->table, badcol, cols[i]);
    cpl_free(badcol);
  } /* for i (all relevant table columns) */

  return CPL_ERROR_NONE;
} /* muse_test_resampling_spectrum() */

static void
muse_test_resampling_image_stats(cpl_image *aImage, const char *aName)
{
  /* same tests as on central plane */
  cpl_stats *stats = cpl_stats_new_from_image(aImage, CPL_STATS_ALL);
  cpl_msg_debug(__func__, "%s: average: %f+/-%f, min/max: %f/%f, flux: %f",
                aName,
                cpl_stats_get_mean(stats), cpl_stats_get_stdev(stats),
                cpl_stats_get_min(stats), cpl_stats_get_max(stats),
                cpl_stats_get_flux(stats));
  cpl_test(cpl_stats_get_mean(stats) > kComparisonImageMin[TMEAN] &&
           cpl_stats_get_mean(stats) < kComparisonImageMax[TMEAN]);
  cpl_test(cpl_stats_get_stdev(stats) > kComparisonImageMin[TSTD] &&
           cpl_stats_get_stdev(stats) < kComparisonImageMax[TSTD]);
  cpl_test(cpl_stats_get_min(stats) > kComparisonImageMin[TMIN] &&
           cpl_stats_get_min(stats) < kComparisonImageMax[TMIN]);
  cpl_test(cpl_stats_get_max(stats) > kComparisonImageMin[TMAX] &&
           cpl_stats_get_max(stats) < kComparisonImageMax[TMAX]);
  cpl_test(cpl_stats_get_flux(stats) > kComparisonImageMin[TFLUX] &&
           cpl_stats_get_flux(stats) < kComparisonImageMax[TFLUX]);
  cpl_stats_delete(stats);
} /* muse_test_resampling_image_stats() */

static cpl_error_code
muse_test_resampling_image(muse_pixtable *aPt, muse_pixtable *aPtGeo)
{
#if 0 /* to ease preparation of a new pixel table for this test */
    /* restrict to wavelength range and to slices 1 to 3 */
    muse_pixtable *ptest = aPt;
    muse_pixtable_restrict_wavelength(ptest, 5745, 6000);
    cpl_table_unselect_all(ptest->table);
    int itest;
    for (itest = 0; itest < cpl_table_get_nrow(ptest->table); itest++) {
      if (muse_pixtable_origin_get_slice(cpl_table_get_int(ptest->table,
                                                           "origin", itest,
                                                           NULL)) > 3) {
        cpl_table_select_row(ptest->table, itest);
      }
    }
    cpl_table_erase_selected(ptest->table);
    muse_pixtable_save(ptest, BASEFILENAME_IMAGE"_p_new.fits");
    exit(111);
#endif
  cpl_msg_info(__func__, "using pixel table with %"CPL_SIZE_FORMAT" rows",
               muse_pixtable_get_nrow(aPt));
  muse_pixtable *pts[] = { aPt, aPtGeo, NULL };

  /* only nearest and renka are supported for resampling to image */
  muse_resampling_type t;
  for (t = MUSE_RESAMPLE_NEAREST; t <= MUSE_RESAMPLE_WEIGHTED_RENKA; t++) {
    muse_pixtable *p;
    int i;
    for (i = 0, p = pts[i]; p; p = pts[++i]) {
      int issimple = muse_pixtable_get_type(p) == MUSE_PIXTABLE_TYPE_SIMPLE;
      cpl_msg_info(__func__, "Resampling to image, method %d, with %s "
                   "geometry info", t, issimple ? "simple" : "full");
      double cputime1 = cpl_test_get_cputime(),
             time1 = cpl_test_get_walltime();
      muse_image *image = muse_resampling_image(p, t, 1., 1.5);
      double cputime2 = cpl_test_get_cputime(),
             time2 = cpl_test_get_walltime();
      cpl_msg_debug(__func__, "...done took %gs (CPU) %gs (wall-clock)",
                    cputime2 - cputime1, time2 - time1);
      cpl_test(image != NULL);
      cpl_test(image->data != NULL);
      cpl_test(image->header != NULL);
      char *name = cpl_sprintf("method %d geo %s", t,
                               issimple ? "simple" : "full");
      muse_test_resampling_image_stats(image->data, name);
      cpl_free(name);

#if 0
      char *fn = cpl_sprintf("image_resampled_%d_geo%s.fits", t,
                             issimple ? "simple" : "full");
      cpl_image_save(image->data, fn, CPL_TYPE_FLOAT, image->header,
                     CPL_IO_CREATE);
      cpl_free(fn);
#endif
      muse_image_delete(image);
    } /* for p (all pixel tables) */
  } /* for t (the two allowed resampling methods) */

  /* test failure cases */
  cpl_errorstate state = cpl_errorstate_get();
  muse_image *image = muse_resampling_image(NULL, MUSE_RESAMPLE_NEAREST, 1., 1.5);
  cpl_test_null(image);
  cpl_errorstate_set(state);
  image = muse_resampling_image(aPt, MUSE_RESAMPLE_NONE, 1., 1.5);
  cpl_test_null(image);
  cpl_errorstate_set(state);

  return CPL_ERROR_NONE;
} /* muse_test_resampling_image() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Test program to check that function from muse_resampling,
           muse_datacube and muse_pixgrid work as expected when called with the
           necessary data.

  This program explicitely tests
    muse_resampling_params_new
    muse_resampling_params_set_wcs
    muse_resampling_params_delete
    muse_resampling_cube
    muse_resampling_euro3d
    muse_resampling_collapse_pixgrid
    muse_resampling_image
    muse_resampling_spectrum
    muse_resampling_spectrum_iterate
    muse_datacube_collapse
    muse_datacube_convert_dq
    muse_datacube_save
    muse_datacube_save_recimages
    muse_datacube_load
    muse_datacube_concat
    muse_euro3dcube_collapse
    muse_euro3dcube_save
    muse_datacube_delete
    muse_euro3dcube_delete
    muse_pixgrid_create
 */
/*----------------------------------------------------------------------------*/
int main(int argc, char **argv)
{
  cpl_test_init(PACKAGE_BUGREPORT, CPL_MSG_DEBUG);

  /* testing of muse_resampling_cube*() */
  /* load pixel table, it has spatial units in pixels */
  muse_pixtable *ptpix = NULL;
  if (argc > 1) {
    /* assume that the 1st parameter is the filename of a pixel table */
    ptpix = muse_pixtable_load(argv[1]);
  } else {
    ptpix = muse_pixtable_load(BASEFILENAME_CUBE"_pixtable.fits");
  }
  if (!ptpix) {
    return cpl_test_end(1);
  }
  muse_test_resampling_cube(ptpix);

  /* also test of muse_resampling_spectrum() with the same pixel table */
  muse_test_resampling_spectrum(ptpix);
  muse_pixtable_delete(ptpix);

  /* testing of muse_resampling_image() */
  muse_pixtable *pt = NULL, *ptgeo = NULL;
  if (argc > 2) {
    /* assume that the 2nd parameter is the filename of another pixel table */
    pt = muse_pixtable_load(argv[2]);
  } else {
    pt = muse_pixtable_load(BASEFILENAME_IMAGE"_pt.fits");
    ptgeo = muse_pixtable_load(BASEFILENAME_IMAGE"_ptgeo.fits");
  }
  if (!pt) {
    return cpl_test_end(1);
  }
  muse_test_resampling_image(pt, ptgeo);
  muse_pixtable_delete(pt);
  muse_pixtable_delete(ptgeo);

  return cpl_test_end(0);
}
