/* -*- 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 mkdtemp() from stdlib.h */
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h> /* stat() */
#include <string.h> /* strcmp() */

#include <muse.h>

#define DIR_TEMPLATE "/tmp/muse_pixtable_test_XXXXXX"

/* compare function for cpl_propertylist_sort() */
static int
muse_test_pixtable_compare_name(const cpl_property *p1,
                                const cpl_property *p2)
{
    return strcmp(cpl_property_get_name(p1),
                  cpl_property_get_name(p2));
}

/* compare MUSE pixel table structure, entries, and header */
static void
muse_test_pixtable_compare(const muse_pixtable *aPt1,
                           const muse_pixtable *aPt2)
{
  /* compare table (contents) */
  cpl_test_nonnull(aPt1);
  cpl_test_nonnull(aPt2);
  if (!aPt1 || !aPt2) {
    return; /* no sense to continue without tables... */
  }
  cpl_test(cpl_table_compare_structure(aPt1->table, aPt2->table) == 0);
  cpl_size nrow1 = cpl_table_get_nrow(aPt1->table),
           nrow2 = cpl_table_get_nrow(aPt1->table);
  cpl_test(nrow1 == nrow2);
  cpl_array *columns = cpl_table_get_column_names(aPt1->table);
  cpl_size icol;
  for (icol = 0; icol < cpl_array_get_size(columns); icol++) {
    const char *colname = cpl_array_get_string(columns, icol);
    /* don't test all entries (takes too long!) just 10 first and 10 last ones */
    cpl_size irow;
    for (irow = 0; irow < 10 && irow < nrow1; irow++) {
      cpl_test(cpl_table_get(aPt1->table, colname, irow, NULL)
               == cpl_table_get(aPt2->table, colname, irow, NULL));
    } /* for irow */
    for (irow = nrow1 - 10; irow < nrow1; irow++) {
      cpl_test(cpl_table_get(aPt1->table, colname, irow, NULL)
               == cpl_table_get(aPt2->table, colname, irow, NULL));
    } /* for irow */
  } /* for icol */
  cpl_array_delete(columns);

  /* compare the headers, but only partially, since on reload *
   * other properties might be there (BIXPIX, SIMPLE, etc.)   */
  cpl_propertylist *h1 = cpl_propertylist_duplicate(aPt1->header),
                   *h2 = cpl_propertylist_duplicate(aPt2->header);
  cpl_propertylist_erase_regexp(h1, "DATE-OBS|EXPTIME|MUSE|PIXTABLE", 1);
  cpl_propertylist_erase_regexp(h2, "DATE-OBS|EXPTIME|MUSE|PIXTABLE", 1);
  cpl_propertylist_sort(h1, muse_test_pixtable_compare_name);
  cpl_propertylist_sort(h2, muse_test_pixtable_compare_name);
  cpl_size n1 = cpl_propertylist_get_size(h1),
           n2 = cpl_propertylist_get_size(h1);
  cpl_test(n1 == n2);
  cpl_size iprop;
  for (iprop = 0; iprop < n1; iprop++) {
    cpl_property *p1 = cpl_propertylist_get(h1, iprop),
                 *p2 = cpl_propertylist_get(h2, iprop);
    cpl_test_eq_string(cpl_property_get_name(p1), cpl_property_get_name(p2));
    if (cpl_property_get_comment(p1)) { /* test first 5 char of non-NULL comments */
      cpl_test_nonnull(cpl_property_get_comment(p2));
      if (cpl_property_get_comment(p2)) {
        cpl_test_zero(strncmp(cpl_property_get_comment(p1),
                              cpl_property_get_comment(p2), 5));
      }
    }
    /* not going to test all possible types and not the types  *
     * themselves, because on re-load they might have changed! */
    if (cpl_property_get_type(p1) == CPL_TYPE_DOUBLE &&
        cpl_property_get_type(p2) == CPL_TYPE_DOUBLE) {
      cpl_test_eq(cpl_property_get_double(p1), cpl_property_get_double(p2));
    } else if (cpl_property_get_type(p1) == CPL_TYPE_INT &&
               cpl_property_get_type(p2) == CPL_TYPE_INT) {
      cpl_test_eq(cpl_property_get_int(p1), cpl_property_get_int(p2));
    }
  } /* for iprop */
  cpl_propertylist_delete(h1);
  cpl_propertylist_delete(h2);

  /* compare the flat-field spectra, if they are present */
  if (aPt1->ffspec) {
    cpl_test_nonnull(aPt2->ffspec);
    cpl_test_zero(cpl_table_compare_structure(aPt1->ffspec, aPt2->ffspec));
    cpl_test_eq(cpl_table_get_nrow(aPt1->ffspec),
                cpl_table_get_nrow(aPt2->ffspec));
  }
} /* muse_test_pixtable_compare() */

static cpl_table *
muse_test_pixtable_create_exptable(const char *aDir, cpl_table *aFFSpec)
{
  /* create one small pixel table to use and save */
#define PTLEN 10
  muse_pixtable *pt = cpl_calloc(1, sizeof(muse_pixtable));
  pt->table = muse_cpltable_new(muse_pixtable_def, PTLEN);
  cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_XPOS, 0, PTLEN, 0.);
  cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_YPOS, 0, PTLEN, 0.);
  cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_LAMBDA, 0, PTLEN/3, 4800.);
  cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_LAMBDA, PTLEN/3, 2*PTLEN/3,
                                     5000.);
  cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_LAMBDA, 2*PTLEN/3, PTLEN/3+1,
                                     5500.);
  pt->header = cpl_propertylist_new();
  cpl_propertylist_append_string(pt->header, MUSE_HDR_PT_TYPE, MUSE_PIXTABLE_STRING_FULL);
  if (aFFSpec) {
    pt->ffspec = cpl_table_duplicate(aFFSpec);
  }

  /* create exposure table a la muse_processing_sort_exposures() */
#define TABLENUM 4
  cpl_table *exptable = cpl_table_new(TABLENUM);
  cpl_table_new_column(exptable, "DATE-OBS", CPL_TYPE_STRING);
  const char *dates[] = { "2013-08-23T12:00:00.000",
                          "2007-10-27T00:13:00.010",
                          "2009-05-18T10:00:24.005" };
  cpl_table_set_string(exptable, "DATE-OBS", 0, dates[0]);
  cpl_table_set_string(exptable, "DATE-OBS", 1, dates[1]);
  cpl_table_set_string(exptable, "DATE-OBS", 2, dates[2]);
  char colname[3]; /* further used in this function, define outside loop */
  int i;
  for (i = 0; i <= kMuseNumIFUs; i++) {
    snprintf(colname, 3, "%02d", i);
    cpl_table_new_column(exptable, colname, CPL_TYPE_STRING);
    if (i == 0) {
      continue; /* don't fill the combined column yet */
    }
    /* fill the table */
    int j;
    for (j = 0; j < TABLENUM - 2; j++) {
      if (j == 1 && (i == 12 || i == 19)) {
        continue; /* skip two IFUs in the 2nd exposure */
      }
      char *fn = cpl_sprintf("%s/pt_%04d-%02d.fits", aDir, j+1, i);
      cpl_table_set_string(exptable, colname, j, fn);

      /* update the pixel table (data, variance, and header) *
       * for this IFU / exposure and save it                 */
      cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_DATA, 0, PTLEN, 1.);
      cpl_table_fill_column_window_float(pt->table, MUSE_PIXTABLE_STAT, 0, PTLEN, 0.5);
      cpl_propertylist_update_string(pt->header, "DATE-OBS", dates[j]);
      cpl_propertylist_update_double(pt->header, MUSE_HDR_FLAT_FLUX_LAMP, 9.8e10);
      if (j == 1) {
        cpl_propertylist_erase(pt->header, MUSE_HDR_FLAT_FLUX_SKY);
      }
      if (aFFSpec) {
        /* multiply two cases by a factor to see if averaging works (should *
         * result in 1.0083333 of the mean of the pre-existing ff-spectrum) */
        if (i == 9) {
          cpl_table_multiply_scalar(pt->ffspec, MUSE_PIXTABLE_FFDATA, 0.9);
        }
        if (i == 13) {
          cpl_table_multiply_scalar(pt->ffspec, MUSE_PIXTABLE_FFDATA, 1.3);
        }
      }
      muse_pixtable_compute_limits(pt);
      muse_pixtable_save(pt, fn);
      cpl_free(fn);
      if (aFFSpec) {
        /* invert the multiplication again! */
        if (i == 9) {
          cpl_table_divide_scalar(pt->ffspec, MUSE_PIXTABLE_FFDATA, 0.9);
        }
        if (i == 13) {
          cpl_table_divide_scalar(pt->ffspec, MUSE_PIXTABLE_FFDATA, 1.3);
        }
      }

      /* for one table, also save a version without ffspec */
      if (aFFSpec && j == 0 && i == 10) {
        cpl_table_delete(pt->ffspec);
        pt->ffspec = NULL;
        char *fnbad = cpl_sprintf("%s/pt_%04d-%02d_bad.fits", aDir, j+1, i);
        muse_pixtable_save(pt, fnbad);
        pt->ffspec = cpl_table_duplicate(aFFSpec); /* reinstate it */
        cpl_free(fnbad);
      }
    } /* for j (rows) */
  } /* for i (columns) */
  /* create combined table in last row */
  char *fn = cpl_sprintf("%s/pt_combined_missing.fits", aDir);
  cpl_table_set_string(exptable, "00", TABLENUM - 2, fn);
  cpl_free(fn);
  fn = cpl_sprintf("%s/pt_combined.fits", aDir);
  cpl_table_set_string(exptable, "00", TABLENUM - 1, fn);
  cpl_propertylist_erase(pt->header, MUSE_HDR_FLAT_FLUX_SKY);
  cpl_propertylist_erase(pt->header, MUSE_HDR_FLAT_FLUX_LAMP);
  if (aFFSpec) {
    cpl_propertylist_update_int(pt->header, MUSE_HDR_PT_FFCORR, 24);
    cpl_propertylist_set_comment(pt->header, MUSE_HDR_PT_FFCORR,
                                 MUSE_HDR_PT_FFCORR_COMMENT);
    /* multiply again as for 24 pixel tables with the scales above */
    cpl_table_multiply_scalar(pt->ffspec, MUSE_PIXTABLE_FFDATA, 24.2 / 24.);
  }
  muse_pixtable_save(pt, fn);
  muse_pixtable_delete(pt);
  cpl_free(fn);

#if 1
  printf("exptable (%s ffspec):\n", aFFSpec ? "with" : "without");
  cpl_table_dump(exptable, 0, 10, stdout);
  fflush(stdout);
#endif
  return exptable;
} /* muse_test_pixtable_create_exptable() */

static void
muse_test_pixtable_delete_exptable(cpl_table *aExpTable)
{
  char colname[3]; /* further used in this function, define outside loop */
  int i;
  for (i = 0; i <= kMuseNumIFUs; i++) {
    snprintf(colname, 3, "%02d", i);
    int j, n = cpl_table_get_nrow(aExpTable);
    for (j = 0; j < n; j++) {
      const char *fn = cpl_table_get_string(aExpTable, colname, j);
      if (fn) {
        cpl_msg_debug(__func__, "removing \"%s\"", fn);
        remove(fn);
      }
    } /* for j (rows) */
  } /* for i (columns) */
  cpl_table_delete(aExpTable);
} /* muse_test_pixtable_delete_exptable() */

/*----------------------------------------------------------------------------*/
/**
  @brief   Test program to check that the functions from the muse_pixtable
           module work when called with the necessary data.

  This program explicitely tests
    muse_pixtable_origin_encode
    muse_pixtable_origin_get_y
    muse_pixtable_origin_get_ifu
    muse_pixtable_origin_get_slice
    muse_pixtable_create
    muse_pixtable_delete
    muse_pixtable_append_ff
    muse_pixtable_save
    muse_pixtable_load_window
    muse_pixtable_load
    muse_pixtable_load_restricted_wavelength
    muse_pixtable_load_merge_channels
    muse_pixtable_duplicate
    muse_pixtable_get_type
    muse_pixtable_flux_multiply
    muse_pixtable_spectrum_apply
    muse_pixtable_wcs_check (only failure cases)
    muse_pixtable_erase_ifu_slice
    muse_pixtable_is_fluxcal
    muse_pixtable_is_skysub
    muse_pixtable_is_rvcorr
    muse_pixtable_to_imagelist
    muse_pixtable_from_imagelist
    muse_pixtable_extracted_get_slices (only failure case)
    muse_pixtable_extracted_get_size (only failure case)
    muse_pixtable_extracted_delete (only failure case)
 */
/*----------------------------------------------------------------------------*/
int main(int argc, char **argv)
{
  UNUSED_ARGUMENTS(argc, argv);
  cpl_test_init(PACKAGE_BUGREPORT, CPL_MSG_DEBUG);

  /* create temporary dir */
  char dirtemplate[FILENAME_MAX] = DIR_TEMPLATE;
  char *dir = mkdtemp(dirtemplate);

  /* test origin en-/decoding around some ranges */
  cpl_errorstate ps = cpl_errorstate_get();
  int xrange[] = { 1, 2077, 4224, 8191, -1 }, /* use int to be able to use */
      yrange[] = { 1, 2199, 4240, 8191, -1 }, /* negative as end-of-list */
      ifurange[] = { 1, 13, kMuseNumIFUs, -1 },
      slicerange[] = { 1, 27, kMuseSlicesPerCCD, -1 },
      offrange[] = { 0, 10, 50, 100, 500, 8191, -1 };
  uint32_t origin;
  unsigned int y;
  unsigned char ifu;
  unsigned short slice;
  int ix, iy, ii, is, io;
  for (ix = 0 ; xrange[ix] >= 0; ix++) {
    for (iy = 0 ; yrange[iy] >= 0; iy++) {
      for (ii = 0 ; ifurange[ii] >= 0; ii++) {
        for (is = 0 ; slicerange[is] >= 0; is++) {
          for (io = 0 ; offrange[io] >= 0; io++) {
            ps = cpl_errorstate_get();
            origin = muse_pixtable_origin_encode(xrange[ix], yrange[iy],
                                                 ifurange[ii], slicerange[is],
                                                 offrange[io]);
            cpl_test(cpl_errorstate_is_equal(ps) && origin);
            if (!cpl_errorstate_is_equal(ps)) {
              cpl_msg_debug(__func__, "%u %u %hhu %hu %u --> %u", xrange[ix],
                            yrange[iy], ifurange[ii], slicerange[is],
                            offrange[io], origin);
            }
            // XXX this is not so simple to test as it needs a
            //     pixel table with full header keywords set up
            // unsigned int x = muse_pixtable_origin_get_x(origin, pt, irow);
            // XXX the same applies to
            // muse_pixtable_origin_set_offset
            // muse_pixtable_origin_get_offset
            // muse_pixtable_origin_copy_offsets
            // muse_pixtable_get_expnum
            y = muse_pixtable_origin_get_y(origin);
            cpl_test(y == (unsigned int)yrange[iy]);
            ifu = muse_pixtable_origin_get_ifu(origin);
            cpl_test(ifu == ifurange[ii]);
            slice = muse_pixtable_origin_get_slice(origin);
            cpl_test(slice == slicerange[is]);
          } /* for io */
        } /* for is */
      } /* for ii */
    } /* for iy */
  } /* for ix */
  /* test failure cases */
  ps = cpl_errorstate_get();
  /* bad x */
  origin = muse_pixtable_origin_encode(0, 1, 1, 1, 1);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT && origin == 0);
  cpl_errorstate_set(ps);
  origin = muse_pixtable_origin_encode(8192, 1, 1, 1, 1);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT && origin == 0);
  cpl_errorstate_set(ps);
  /* bad y */
  origin = muse_pixtable_origin_encode(1, 0, 1, 1, 1);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT && origin == 0);
  cpl_errorstate_set(ps);
  origin = muse_pixtable_origin_encode(1, 8192, 1, 1, 1);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT && origin == 0);
  cpl_errorstate_set(ps);
  /* bad ifu */
  origin = muse_pixtable_origin_encode(1, 1, 0, 1, 1);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT && origin == 0);
  cpl_errorstate_set(ps);
  origin = muse_pixtable_origin_encode(1, 1, kMuseNumIFUs + 1, 1, 1);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT && origin == 0);
  cpl_errorstate_set(ps);
  /* bad slice */
  origin = muse_pixtable_origin_encode(1, 1, 1, 0, 1);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT && origin == 0);
  cpl_errorstate_set(ps);
  origin = muse_pixtable_origin_encode(1, 1, 1, kMuseSlicesPerCCD + 1, 1);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT && origin == 0);
  cpl_errorstate_set(ps);
  /* bad offset */
  origin = muse_pixtable_origin_encode(1, 1, 1, 1, -1);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT && origin == 0);
  cpl_errorstate_set(ps);
  origin = muse_pixtable_origin_encode(1, 1, 1, 1, 8192);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT && origin == 0);
  cpl_errorstate_set(ps);
  /* bad origins */
  y = muse_pixtable_origin_get_y(0);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_OUTPUT && y == 0);
  cpl_errorstate_set(ps);
  ifu = muse_pixtable_origin_get_ifu(0);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_OUTPUT && ifu == 0);
  cpl_errorstate_set(ps);
  slice = muse_pixtable_origin_get_slice(0);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_OUTPUT && slice == 0);
  cpl_errorstate_set(ps);

  /* test muse_pixtable_create() */
  cpl_table *wavecaltable = cpl_table_load(BASEFILENAME"_create_wavecal_in.fits", 1, 1);
  cpl_table *tracetable = cpl_table_load(BASEFILENAME"_create_trace_in.fits", 1, 1);
  cpl_table *geotable = cpl_table_load(BASEFILENAME"_create_geotable.fits", 1, 1);
  /* just take any image into which the normal MUSE pattern fits */
  muse_image *inimage = muse_image_load(BASEFILENAME"_create_data.fits");

  /* inimage does not contain a BUNIT, this should first fail because of that */
  ps = cpl_errorstate_get();
  muse_pixtable *pt = muse_pixtable_create(inimage, tracetable, wavecaltable,
                                           geotable);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_null(pt);
  /* add a wrong BUNIT, should still fail the same */
  cpl_propertylist_append_string(inimage->header, "BUNIT", "adu");
  pt = muse_pixtable_create(inimage, tracetable, wavecaltable, geotable);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(ps);
  /* add the correct BUNIT, now fails because of the geotable without "width" */
  cpl_propertylist_update_string(inimage->header, "BUNIT", "count");
  pt = muse_pixtable_create(inimage, tracetable, wavecaltable, geotable);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_BAD_FILE_FORMAT);
  cpl_errorstate_set(ps);
  /* try again, now with a "width" column */
  cpl_table_new_column(geotable, MUSE_GEOTABLE_WIDTH, CPL_TYPE_DOUBLE);
  cpl_table_fill_column_window(geotable, MUSE_GEOTABLE_WIDTH,
                               0, cpl_table_get_nrow(geotable),
                               kMuseSliceNominalWidth);
  pt = muse_pixtable_create(inimage, tracetable, wavecaltable, geotable);
  cpl_test_nonnull(pt);
  cpl_test(muse_pixtable_get_type(pt) == MUSE_PIXTABLE_TYPE_FULL);
  cpl_size nrows = muse_pixtable_get_nrow(pt);
  cpl_msg_info(__func__, "created pixel table with %"CPL_SIZE_FORMAT" rows", nrows);
  /* the table of one IFU should contain a min number of pixels that is *
   * based on the number of slices and their nominal width and a max of *
   * the number of CCD pixels                                           */
  cpl_test(nrows >= 48*75*cpl_image_get_size_y(inimage->data));
  cpl_test(nrows <= cpl_image_get_size_x(inimage->data)
                    * cpl_image_get_size_y(inimage->data));
  /* keep this pixel table for the test of converting into an imagelist */
  muse_pixtable *ptkeep = muse_pixtable_duplicate(pt);
  /* test a few more failure cases */
  ps = cpl_errorstate_get();
  muse_pixtable *pt2 = muse_pixtable_create(NULL, tracetable, wavecaltable,
                                            geotable);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_null(pt2);
  ps = cpl_errorstate_get();
  muse_image *inimage2 = muse_image_duplicate(inimage);
  cpl_propertylist_erase_regexp(inimage2->header, "EXTNAME", 0);
  pt2 = muse_pixtable_create(inimage2, tracetable, wavecaltable, geotable);
  muse_image_delete(inimage2);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(ps);
  cpl_test_null(pt2);
  ps = cpl_errorstate_get();
  pt2 = muse_pixtable_create(inimage, NULL, wavecaltable, geotable);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_null(pt2);
  ps = cpl_errorstate_get();
  pt2 = muse_pixtable_create(inimage, tracetable, NULL, geotable);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_null(pt2);
  /* a NULL geometry table should succeed */
  ps = cpl_errorstate_get();
  pt2 = muse_pixtable_create(inimage, tracetable, wavecaltable, NULL);
  cpl_test(cpl_errorstate_is_equal(ps));
  cpl_test_nonnull(pt2);
  cpl_test(muse_pixtable_get_type(pt2) == MUSE_PIXTABLE_TYPE_SIMPLE);
  /* A partially broken trace table should succeed, too, but       *
   * only contain that part of the slice that was not well-defined. *
   * To set this test up, use the example from slice 46 of IFU 24   *
   * as originally traced in January 2014 on Paranal, at least using *
   * the coefficients that are present in this testing tracetable.   */
  cpl_table *tr46 = cpl_table_extract(tracetable, 45, 1);
  cpl_table_set(tracetable, "Width", 45, 78.58753);
  cpl_table_set(tracetable, "tc0_00", 45, 3848.23931839034);
  cpl_table_set(tracetable, "tc0_01", 45, -0.000178341417506141);
  cpl_table_set(tracetable, "tc0_02", 45, 1.10609075533106E-05);
  cpl_table_set(tracetable, "tc0_03", 45, -1.73609539085184E-08);
  /* cpl_table_set(tracetable, "tc0_04", 45, 8.87842838335494E-12); */
  /* cpl_table_set(tracetable, "tc0_05", 45, -1.51337333904576E-15); */
  cpl_table_set(tracetable, "MSE0", 45, 4.96537658905216E-07);
  cpl_table_set(tracetable, "tc1_00", 45, 3808.95529212496);
  cpl_table_set(tracetable, "tc1_01", 45, 0.00262511193392701);
  cpl_table_set(tracetable, "tc1_02", 45, -1.08439315040227E-06);
  cpl_table_set(tracetable, "tc1_03", 45, -3.14353356737499E-09);
  /* cpl_table_set(tracetable, "tc1_04", 45, 2.31766748107034E-12); */
  /* cpl_table_set(tracetable, "tc1_05", 45, -4.46730257366279E-16); */
  cpl_table_set(tracetable, "MSE1", 45, 2.03588680227243E-06);
  cpl_table_set(tracetable, "tc2_00", 45, 3887.5233446837);
  cpl_table_set(tracetable, "tc2_01", 45, -0.00298179511483656);
  cpl_table_set(tracetable, "tc2_02", 45, 2.32062095346736E-05);
  cpl_table_set(tracetable, "tc2_03", 45, -3.15783757320432E-08);
  /* cpl_table_set(tracetable, "tc2_04", 45, 1.54391899749506E-11); */
  /* cpl_table_set(tracetable, "tc2_05", 45, -2.58001653375017E-15); */
  cpl_table_set(tracetable, "MSE2", 45, 1.6756966321601E-07);
  ps = cpl_errorstate_get();
  muse_pixtable *pt5 = muse_pixtable_create(inimage, tracetable, wavecaltable, geotable);
  cpl_test(cpl_errorstate_is_equal(ps));
  cpl_test_nonnull(pt5);
  /* check that the resultung part for table for slice 46 stops *
   * at around 6700 Angstrom, while slice 45 and 47 go to 9300  */
  muse_pixtable **pts5 = muse_pixtable_extracted_get_slices(pt5);
  double lambda45 = cpl_table_get(pts5[44]->table, MUSE_PIXTABLE_LAMBDA,
                                  muse_pixtable_get_nrow(pts5[44]) - 1, NULL),
         lambda46 = cpl_table_get(pts5[45]->table, MUSE_PIXTABLE_LAMBDA,
                                  muse_pixtable_get_nrow(pts5[45]) - 1, NULL),
         lambda47 = cpl_table_get(pts5[46]->table, MUSE_PIXTABLE_LAMBDA,
                                  muse_pixtable_get_nrow(pts5[46]) - 1, NULL);
  cpl_msg_debug(__func__, "last wavelength in slice 45/46/47: %f/%f/%f",
                lambda45, lambda46, lambda47);
  cpl_test(lambda46 < 6700.);
  cpl_test(lambda45 > 9300. && lambda47 > 9300.);
  muse_pixtable_extracted_delete(pts5);
  muse_pixtable_delete(pt5);
  /* test with completely missing entry for one slice 46, this should *
   * propagate error from muse_trace_table_get_polys_for_slice() but  *
   * otherwise get good results for 47 slices                         */
  cpl_table_erase_window(tracetable, 45, 1);
  /* should fail without MUSE_EXPERT_USER */
  ps = cpl_errorstate_get();
  pt5 = muse_pixtable_create(inimage, tracetable, wavecaltable, geotable);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_null(pt5);
  /* should succeed partially with MUSE_EXPERT_USER=1 */
  cpl_test_zero(setenv("MUSE_EXPERT_USER", "1", 1));
  ps = cpl_errorstate_get();
  pt5 = muse_pixtable_create(inimage, tracetable, wavecaltable, geotable);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_nonnull(pt5);
  cpl_test_zero(unsetenv("MUSE_EXPERT_USER"));
  pts5 = muse_pixtable_extracted_get_slices(pt5);
  cpl_test(muse_pixtable_extracted_get_size(pts5) == 47); /* one missing slice */
  muse_pixtable_extracted_delete(pts5);
  muse_pixtable_delete(pt5);
  cpl_table_insert(tracetable, tr46, 45); /* put that missing row back */
  cpl_table_delete(tr46);
  /* try with broken wavecal table (one slice missing) */
  cpl_table *wv46 = cpl_table_extract(wavecaltable, 45, 1);
  cpl_table_erase_window(wavecaltable, 45, 1);
  /* should fail without MUSE_EXPERT_USER */
  ps = cpl_errorstate_get();
  muse_pixtable *pt6 = muse_pixtable_create(inimage, tracetable, wavecaltable, geotable);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_null(pt6);
  /* should succeed partially with MUSE_EXPERT_USER=1 */
  cpl_test_zero(setenv("MUSE_EXPERT_USER", "1", 1));
  ps = cpl_errorstate_get();
  pt6 = muse_pixtable_create(inimage, tracetable, wavecaltable, geotable);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_nonnull(pt6);
  cpl_test_zero(unsetenv("MUSE_EXPERT_USER"));
  muse_pixtable **pts6 = muse_pixtable_extracted_get_slices(pt6);
  cpl_test(muse_pixtable_extracted_get_size(pts6) == 47); /* one missing slice */
  muse_pixtable_extracted_delete(pts6);
  muse_pixtable_delete(pt6);
  cpl_table_insert(wavecaltable, wv46, 45); /* put that missing row back */
  /* try with wavecal for one slice giving unlikely values */
  cpl_table_set(wavecaltable, "wlc00", 45, 2000.);
  cpl_table_set(wavecaltable, "wlc01", 45, 0.);
  cpl_table_set(wavecaltable, "wlc02", 45, 0.);
  cpl_table_set(wavecaltable, "wlc03", 45, 0.);
  cpl_table_set(wavecaltable, "wlc04", 45, 0.);
  cpl_table_set(wavecaltable, "wlc05", 45, 0.);
  cpl_table_set(wavecaltable, "wlc10", 45, 0.);
  cpl_table_set(wavecaltable, "wlc11", 45, 0.);
  cpl_table_set(wavecaltable, "wlc12", 45, 0.);
  cpl_table_set(wavecaltable, "wlc13", 45, 0.);
  cpl_table_set(wavecaltable, "wlc14", 45, 0.);
  cpl_table_set(wavecaltable, "wlc15", 45, 0.);
  cpl_table_set(wavecaltable, "wlc20", 45, 0.);
  cpl_table_set(wavecaltable, "wlc21", 45, 0.);
  cpl_table_set(wavecaltable, "wlc22", 45, 0.);
  cpl_table_set(wavecaltable, "wlc23", 45, 0.);
  cpl_table_set(wavecaltable, "wlc24", 45, 0.);
  cpl_table_set(wavecaltable, "wlc25", 45, 0.);
  cpl_table_set(wavecaltable, "MSE", 45, 1000.);
  ps = cpl_errorstate_get();
  pt6 = muse_pixtable_create(inimage, tracetable, wavecaltable, geotable);
  cpl_test(cpl_errorstate_is_equal(ps));
  cpl_errorstate_set(ps);
  cpl_test_nonnull(pt6);
  pts6 = muse_pixtable_extracted_get_slices(pt6);
  cpl_test(muse_pixtable_extracted_get_size(pts6) == 47); /* one missing slice */
  muse_pixtable_extracted_delete(pts6);
  muse_pixtable_delete(pt6);
  cpl_table_erase_window(wavecaltable, 45, 1);
  cpl_table_insert(wavecaltable, wv46, 45); /* put that missing row back */
  cpl_table_delete(wv46);
  /* test with broken geometry table, should completely fail without this IFU */
  cpl_table *geo1a = cpl_table_extract(geotable, 0, 48),
            *geo1b = cpl_table_duplicate(geo1a);
  cpl_table_erase_window(geotable, 0, 48);
  ps = cpl_errorstate_get();
  muse_pixtable *pt7 = muse_pixtable_create(inimage, tracetable, wavecaltable, geotable);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_null(pt7);
  /* without x column */
  cpl_table_erase_column(geo1a, MUSE_GEOTABLE_X);
  ps = cpl_errorstate_get();
  pt7 = muse_pixtable_create(inimage, tracetable, wavecaltable, geo1a);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_null(pt7);
  ps = cpl_errorstate_get();
  /* without y column */
  cpl_table_erase_column(geo1b, MUSE_GEOTABLE_Y);
  ps = cpl_errorstate_get();
  pt7 = muse_pixtable_create(inimage, tracetable, wavecaltable, geo1b);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  /* without x and y columns */
  cpl_table_erase_column(geo1b, MUSE_GEOTABLE_X);
  ps = cpl_errorstate_get();
  pt7 = muse_pixtable_create(inimage, tracetable, wavecaltable, geo1b);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_null(pt7);
  cpl_table_delete(geo1a);
  cpl_table_delete(geo1b);
  cpl_table_delete(geotable);

  /* test muse_pixtable_append_ff(),                                      *
   * use the original image filled with data around 1 (like a flat-field) */
  int nx = cpl_image_get_size_x(inimage->data),
      ny = cpl_image_get_size_y(inimage->data);
  cpl_image_fill_window(inimage->data, 1, 1, nx, ny, 1.);
  cpl_image_fill_window(inimage->stat, 1, 1, nx, ny, 0.5);
  cpl_image_fill_window(inimage->dq, 1, 1, nx, ny, 0);
  cpl_test_null(pt->ffspec); /* no pre-existing flat-field spectrum */
  ps = cpl_errorstate_get();
  cpl_error_code rc = muse_pixtable_append_ff(pt, inimage, tracetable,
                                              wavecaltable,
                                              kMuseSpectralSamplingA);
  cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
  cpl_test_nonnull(pt->ffspec);
  cpl_array *cols = cpl_table_get_column_names(pt->ffspec);
  cpl_test(cpl_array_get_size(cols) == 2);
  cpl_test_zero(strncmp(cpl_array_get_string(cols, 0), MUSE_PIXTABLE_FFLAMBDA,
                        strlen(MUSE_PIXTABLE_FFLAMBDA) + 1));
  cpl_test_zero(strncmp(cpl_array_get_string(cols, 1), MUSE_PIXTABLE_FFDATA,
                        strlen(MUSE_PIXTABLE_FFDATA) + 1));
  cpl_array_delete(cols);
  /* do this again, should replace the ffspec and not cause any memory leaks */
  ps = cpl_errorstate_get();
  rc = muse_pixtable_append_ff(pt, inimage, tracetable, wavecaltable,
                               kMuseSpectralSamplingA);
  cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
  cpl_test_nonnull(pt->ffspec);
  /* detach ff-spectrum from the pixel table for now */
  cpl_table *ffspec = pt->ffspec,
            *ffspec2 = cpl_table_duplicate(ffspec); /* need another instance */
  pt->ffspec = NULL;
  /* generate direct error */
  ps = cpl_errorstate_get();
  rc = muse_pixtable_append_ff(NULL, inimage, tracetable, wavecaltable,
                               kMuseSpectralSamplingA);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  /* generate propagated error */
  rc = muse_pixtable_append_ff(pt, inimage, NULL, wavecaltable,
                               kMuseSpectralSamplingA);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  cpl_table_delete(wavecaltable);
  cpl_table_delete(tracetable);
  muse_image_delete(inimage);

  /* test failure cases of muse_pixtable_get_type() */
  ps = cpl_errorstate_get();
  cpl_test(muse_pixtable_get_type(NULL) == MUSE_PIXTABLE_TYPE_UNKNOWN);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  /* set bad pixel table type header */
  cpl_propertylist_update_string(pt2->header, MUSE_HDR_PT_TYPE, "blabla");
  ps = cpl_errorstate_get();
  cpl_test(muse_pixtable_get_type(pt2) == MUSE_PIXTABLE_TYPE_UNKNOWN);
  cpl_test(cpl_errorstate_is_equal(ps)); /* no new error set */

  /* test muse_pixtable_flux_multiply() */
  ps = cpl_errorstate_get();
  cpl_size nrow2 = muse_pixtable_get_nrow(pt2);
  float d1 = cpl_table_get(pt2->table, MUSE_PIXTABLE_DATA, 0, NULL),
        d2 = cpl_table_get(pt2->table, MUSE_PIXTABLE_DATA, nrow2 / 2, NULL),
        d3 = cpl_table_get(pt2->table, MUSE_PIXTABLE_DATA, nrow2 - 1, NULL);
  rc = muse_pixtable_flux_multiply(pt2, 1.33);
  cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
  cpl_test_rel(cpl_table_get(pt2->table, MUSE_PIXTABLE_DATA, 0, NULL), d1 * 1.33, 5e-8);
  cpl_test_rel(cpl_table_get(pt2->table, MUSE_PIXTABLE_DATA, nrow2 / 2, NULL), d2 * 1.33, 5e-8);
  cpl_test_rel(cpl_table_get(pt2->table, MUSE_PIXTABLE_DATA, nrow2 - 1, NULL), d3 * 1.33, 5e-8);
  rc = muse_pixtable_flux_multiply(pt2, 1. / 1.33); /* reverse it */
  cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
  cpl_test_rel(cpl_table_get(pt2->table, MUSE_PIXTABLE_DATA, 0, NULL), d1, DBL_EPSILON);
  cpl_test_rel(cpl_table_get(pt2->table, MUSE_PIXTABLE_DATA, nrow2 / 2, NULL), d2, DBL_EPSILON);
  cpl_test_rel(cpl_table_get(pt2->table, MUSE_PIXTABLE_DATA, nrow2 - 1, NULL), d3, DBL_EPSILON);
  /* failure cases */
  ps = cpl_errorstate_get();
  rc = muse_pixtable_flux_multiply(NULL, 2.);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  cpl_table_delete(pt2->table);
  pt2->table = NULL;
  ps = cpl_errorstate_get();
  rc = muse_pixtable_flux_multiply(pt2, 2.);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  muse_pixtable_delete(pt2);

  /* test muse_pixtable_spectrum_apply(), using very simple 5-pixel table */
  muse_pixtable *ptmin = cpl_calloc(1, sizeof(muse_pixtable));
  ptmin->table = muse_cpltable_new(muse_pixtable_def, 5);
  cpl_table_fill_column_window_float(ptmin->table, MUSE_PIXTABLE_DATA, 0, 5, 5.);
  cpl_table_fill_column_window_float(ptmin->table, MUSE_PIXTABLE_STAT, 0, 5, 4.);
  cpl_table_set_float(ptmin->table, MUSE_PIXTABLE_LAMBDA, 0, 5000.);
  cpl_table_set_float(ptmin->table, MUSE_PIXTABLE_LAMBDA, 1, 6000.);
  cpl_table_set_float(ptmin->table, MUSE_PIXTABLE_LAMBDA, 2, 7000.);
  cpl_table_set_float(ptmin->table, MUSE_PIXTABLE_LAMBDA, 3, 8000.);
  cpl_table_set_float(ptmin->table, MUSE_PIXTABLE_LAMBDA, 4, 9000.);
  /* the function to test splits up into slices, so we need the origin column */
  cpl_table_set_int(ptmin->table, MUSE_PIXTABLE_ORIGIN, 0,
                    muse_pixtable_origin_encode(100, 100, 1, 2, 50));
  cpl_table_set_int(ptmin->table, MUSE_PIXTABLE_ORIGIN, 1,
                    muse_pixtable_origin_encode(150, 550, 1, 3, 99));
  cpl_table_set_int(ptmin->table, MUSE_PIXTABLE_ORIGIN, 2,
                    muse_pixtable_origin_encode(100, 550, 3, 2, 50));
  cpl_table_set_int(ptmin->table, MUSE_PIXTABLE_ORIGIN, 3,
                    muse_pixtable_origin_encode(500, 550, 15, 9, 400));
  cpl_table_set_int(ptmin->table, MUSE_PIXTABLE_ORIGIN, 4,
                    muse_pixtable_origin_encode(500, 550, 21, 9, 400));
  ptmin->header = cpl_propertylist_new();
  cpl_propertylist_append_string(ptmin->header, "DATE-OBS",
                                 "2016-09-01T18:43:57.041");
#if 0
  printf("=== ptmin ===\n");
  /* use CPL table dump, no pixel table offset headers needed */
  cpl_table_dump(ptmin->table, 0, 10, stdout);
  fflush(stdout);
#endif
  cpl_array *lmin = cpl_array_new(6, CPL_TYPE_DOUBLE),
            *fmin = cpl_array_new(6, CPL_TYPE_DOUBLE);
  cpl_array_set_double(lmin, 0, 5000.);
  cpl_array_set_double(lmin, 1, 6000.);
  cpl_array_set_double(lmin, 2, 7000.);
  cpl_array_set_double(lmin, 3, 8000.);
  cpl_array_set_double(lmin, 4, 9000.);
  cpl_array_set_double(lmin, 5, 10000.);
  cpl_array_fill_window_double(fmin, 0, 6, 4.);
  /* now finally do the tests */
  ps = cpl_errorstate_get();
  rc = muse_pixtable_spectrum_apply(ptmin, lmin, fmin,
                                    MUSE_PIXTABLE_OPERATION_SUBTRACT);
  cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
#if 0
  printf("=== ptmin after - ===\n");
  cpl_table_dump(ptmin->table, 0, 10, stdout); /* use this, no headers needed */
  fflush(stdout);
#endif
  cpl_test_rel(cpl_table_get_column_mean(ptmin->table, MUSE_PIXTABLE_DATA), 1., DBL_EPSILON);
  cpl_test_rel(cpl_table_get_column_stdev(ptmin->table, MUSE_PIXTABLE_DATA), 0., DBL_EPSILON);
  /* variance is unchanged */
  cpl_test_rel(cpl_table_get_column_mean(ptmin->table, MUSE_PIXTABLE_STAT), 4., DBL_EPSILON);
  cpl_test_rel(cpl_table_get_column_stdev(ptmin->table, MUSE_PIXTABLE_STAT), 0., DBL_EPSILON);
  ps = cpl_errorstate_get();
  rc = muse_pixtable_spectrum_apply(ptmin, lmin, fmin,
                                    MUSE_PIXTABLE_OPERATION_MULTIPLY);
  cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
#if 0
  printf("=== ptmin after * ===\n");
  cpl_table_dump(ptmin->table, 0, 10, stdout); /* use this, no headers needed */
  fflush(stdout);
#endif
  cpl_test_rel(cpl_table_get_column_mean(ptmin->table, MUSE_PIXTABLE_DATA), 4., DBL_EPSILON);
  cpl_test_rel(cpl_table_get_column_stdev(ptmin->table, MUSE_PIXTABLE_DATA), 0., DBL_EPSILON);
  /* variance is multiplied by the square */
  cpl_test_rel(cpl_table_get_column_mean(ptmin->table, MUSE_PIXTABLE_STAT), 64., DBL_EPSILON);
  cpl_test_rel(cpl_table_get_column_stdev(ptmin->table, MUSE_PIXTABLE_STAT), 0., DBL_EPSILON);
  /* now shift the wavelengths and try again, with non-constant spectrum */
  cpl_table_add_scalar(ptmin->table, MUSE_PIXTABLE_LAMBDA, 333.3333333);
  cpl_array_set_double(fmin, 0, 1.);
  cpl_array_set_double(fmin, 1, 2.);
  cpl_array_set_double(fmin, 2, 3.);
  cpl_array_set_double(fmin, 3, 4.);
  cpl_array_set_double(fmin, 4, 5.);
  cpl_array_set_double(fmin, 5, 6.);
  ps = cpl_errorstate_get();
  rc = muse_pixtable_spectrum_apply(ptmin, lmin, fmin,
                                    MUSE_PIXTABLE_OPERATION_SUBTRACT);
  cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
#if 0
  printf("=== arrays (2nd) ===\n");
  cpl_array_dump(lmin, 0, 10, stdout);
  cpl_array_dump(fmin, 0, 10, stdout);
  printf("=== ptmin after - (2nd) ===\n");
  cpl_table_dump(ptmin->table, 0, 10, stdout); /* use this, no headers needed */
  fflush(stdout);
#endif
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_DATA, 0, NULL), 4.-4./3., FLT_EPSILON);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_DATA, 1, NULL), 4.-7./3., FLT_EPSILON);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_DATA, 2, NULL), 4.-10./3., FLT_EPSILON*2.1);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_DATA, 3, NULL), 4.-13./3., FLT_EPSILON*8.1);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_DATA, 4, NULL), 4.-16./3., FLT_EPSILON*2.1);
  /* variance is unchanged */
  cpl_test_rel(cpl_table_get_column_mean(ptmin->table, MUSE_PIXTABLE_STAT), 64., DBL_EPSILON);
  cpl_test_rel(cpl_table_get_column_stdev(ptmin->table, MUSE_PIXTABLE_STAT), 0., DBL_EPSILON);
  /* for next test, reset pixel table for easier computations */
  cpl_table_fill_column_window_float(ptmin->table, MUSE_PIXTABLE_DATA, 0, 5, 1.);
  cpl_table_fill_column_window_float(ptmin->table, MUSE_PIXTABLE_STAT, 0, 5, 1.);
  /* keep before multiplication state, for division testing */
  muse_pixtable *ptbm = muse_pixtable_duplicate(ptmin);
  ps = cpl_errorstate_get();
  rc = muse_pixtable_spectrum_apply(ptmin, lmin, fmin,
                                    MUSE_PIXTABLE_OPERATION_MULTIPLY);
  cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
#if 0
  printf("=== ptmin after * (2nd) ===\n");
  cpl_table_dump(ptmin->table, 0, 10, stdout); /* use this, no headers needed */
  fflush(stdout);
#endif
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_DATA, 0, NULL), 4./3., FLT_EPSILON*1.1);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_DATA, 1, NULL), 7./3., FLT_EPSILON);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_DATA, 2, NULL), 10./3., FLT_EPSILON);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_DATA, 3, NULL), 13./3., FLT_EPSILON);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_DATA, 4, NULL), 16./3., FLT_EPSILON);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_STAT, 0, NULL), 16./9., FLT_EPSILON*2.5);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_STAT, 1, NULL), 49./9., FLT_EPSILON*1.7);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_STAT, 2, NULL), 100./9., FLT_EPSILON*1.2);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_STAT, 3, NULL), 169./9., FLT_EPSILON*1.4);
  cpl_test_rel(cpl_table_get(ptmin->table, MUSE_PIXTABLE_STAT, 4, NULL), 256./9., FLT_EPSILON*1.1);
  /* check division by inverting the previous operation */
  ps = cpl_errorstate_get();
  rc = muse_pixtable_spectrum_apply(ptmin, lmin, fmin,
                                    MUSE_PIXTABLE_OPERATION_DIVIDE);
  cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
  cpl_array *data1 = muse_cpltable_extract_column(ptbm->table, MUSE_PIXTABLE_DATA),
            *stat1 = muse_cpltable_extract_column(ptbm->table, MUSE_PIXTABLE_STAT),
            *data2 = muse_cpltable_extract_column(ptmin->table, MUSE_PIXTABLE_DATA),
            *stat2 = muse_cpltable_extract_column(ptmin->table, MUSE_PIXTABLE_STAT);
  cpl_test_array_abs(data1, data2, FLT_EPSILON);
  cpl_test_array_abs(stat1, stat2, FLT_EPSILON);
  cpl_array_unwrap(data1);
  cpl_array_unwrap(stat1);
  cpl_array_unwrap(data2);
  cpl_array_unwrap(stat2);
  muse_pixtable_delete(ptbm);
  /* test failure cases of muse_pixtable_spectrum_apply() */
  ps = cpl_errorstate_get();
  rc = muse_pixtable_spectrum_apply(NULL, lmin, fmin,
                                    MUSE_PIXTABLE_OPERATION_MULTIPLY);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  cpl_table *ttmp = ptmin->table;
  ptmin->table = NULL;
  rc = muse_pixtable_spectrum_apply(NULL, lmin, fmin,
                                    MUSE_PIXTABLE_OPERATION_MULTIPLY);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  ptmin->table = ttmp;
  rc = muse_pixtable_spectrum_apply(ptmin, NULL, fmin,
                                    MUSE_PIXTABLE_OPERATION_MULTIPLY);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  rc = muse_pixtable_spectrum_apply(ptmin, lmin, NULL,
                                    MUSE_PIXTABLE_OPERATION_MULTIPLY);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  cpl_array *fmin2 = cpl_array_duplicate(fmin),
            *lmin2 = cpl_array_duplicate(lmin);
  cpl_array_set_size(fmin2, 10); /* unequal lengths */
  rc = muse_pixtable_spectrum_apply(ptmin, lmin, fmin2,
                                    MUSE_PIXTABLE_OPERATION_MULTIPLY);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  cpl_array_set_size(lmin2, 0); /* equal lengths but not positive */
  cpl_array_set_size(fmin2, 0);
  rc = muse_pixtable_spectrum_apply(ptmin, lmin2, fmin2,
                                    MUSE_PIXTABLE_OPERATION_MULTIPLY);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  cpl_array_delete(lmin2);
  cpl_array_delete(fmin2);
  cpl_array *a1 = cpl_array_new(6, CPL_TYPE_INT), /* with bad types */
            *a2 = cpl_array_new(6, CPL_TYPE_FLOAT);
  rc = muse_pixtable_spectrum_apply(ptmin, a1, fmin,
                                    MUSE_PIXTABLE_OPERATION_MULTIPLY);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(ps);
  rc = muse_pixtable_spectrum_apply(ptmin, lmin, a2,
                                    MUSE_PIXTABLE_OPERATION_MULTIPLY);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_array_delete(a1);
  cpl_array_delete(a2);
  cpl_errorstate_set(ps);
  rc = muse_pixtable_spectrum_apply(ptmin, lmin, fmin,
                                    MUSE_PIXTABLE_OPERATION_DIVIDE + 1);
  cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_UNSUPPORTED_MODE);
  cpl_errorstate_set(ps);
  cpl_array_delete(lmin);
  cpl_array_delete(fmin);
  muse_pixtable_delete(ptmin);

  /* test muse_pixtable_duplicate() */
  ps = cpl_errorstate_get();
  pt2 = muse_pixtable_duplicate(NULL);
  cpl_test(cpl_errorstate_is_equal(ps)); /* no new errors */
  cpl_test_null(pt2);
  ps = cpl_errorstate_get();
  pt2 = muse_pixtable_duplicate(pt); /* test success */
  cpl_test(cpl_errorstate_is_equal(ps));
  cpl_test_nonnull(pt2);
  /* compare first and last entry of the data column to be sure */
  muse_test_pixtable_compare(pt, pt2);
  muse_pixtable *pt2keep = muse_pixtable_duplicate(pt2);

  /* do the following tests twice:                               *
   *   i=0 means pixel table without flat-field spectrum         *
   *   i=1 means pixel table /with/ flat-field spectrum attached */
  char *fnbad = cpl_sprintf("%s/%s", dir, "muse_pixtable_bad_ffspec.fits");
  int i;
  for (i = 0; i <= 1; i++) {
    /* in the 2nd iteration, attach the flat-field spectrum created above */
    if (i == 1) {
      pt->ffspec = ffspec;
      cpl_msg_info(__func__, "_______________________ with flat-field "
                   "spectrum _______________________________");
      /* reinstate pt2 again, which was deleted in the first iteration, *
       * with ff spectrum this time                                     */
      pt2 = pt2keep;
      pt2->ffspec = cpl_table_duplicate(ffspec);
    } else {
      cpl_msg_info(__func__, "_______________________ without flat-field "
                   "spectrum ____________________________");
    }

    /* test muse_pixtable_save() and muse_pixtable_load() as *
     * well as muse_pixtable_load_restricted_wavelength()    */
    cpl_test_zero(setenv("MUSE_PIXTABLE_SAVE_AS_TABLE", "1", 1)); /* bintable */
    char *fn = cpl_sprintf("%s/%s", dir, "muse_pixtable_as_table.fits");
    ps = cpl_errorstate_get();
    rc = muse_pixtable_save(pt, fn);
    cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
    muse_pixtable_delete(pt);
    struct stat sb;
    cpl_test_zero(stat(fn, &sb));
    /* the file written should be a normal file with size greater than zero... */
    cpl_test(S_ISREG(sb.st_mode) && sb.st_size > 0);
    if (i == 0) {
      /* ... and contain one (binary table) extension, no flat-field extension */
      cpl_test(cpl_fits_count_extensions(fn) == 1);
      cpl_test_zero(cpl_fits_find_extension(fn, MUSE_PIXTABLE_FF_EXT));
    } else {
      /* ... and contain two (binary table) extensions, incl. flat-field spec */
      cpl_test(cpl_fits_count_extensions(fn) == 2);
      cpl_test_eq(cpl_fits_find_extension(fn, MUSE_PIXTABLE_FF_EXT), 2);
    }
    /* load it again and compare to its old duplicate */
    ps = cpl_errorstate_get();
    pt = muse_pixtable_load(fn);
    cpl_test(cpl_errorstate_is_equal(ps));
    cpl_test_nonnull(pt);
    muse_test_pixtable_compare(pt2, pt);
    ps = cpl_errorstate_get();
    muse_pixtable *pt2x = muse_pixtable_load_restricted_wavelength(fn, 4000., 10000.);
    cpl_test(cpl_errorstate_is_equal(ps));
    cpl_test_nonnull(pt2x);
    muse_test_pixtable_compare(pt2x, pt); /* full range, should match */
    muse_pixtable_delete(pt2x);
    pt2x = muse_pixtable_load_restricted_wavelength(fn, 4999., 5001.);
    cpl_test(cpl_errorstate_is_equal(ps));
    cpl_test_nonnull(pt2x);
    cpl_test_eq(muse_pixtable_get_nrow(pt2x), 5688);
    cpl_test_abs(cpl_table_get_column_min(pt2x->table, MUSE_PIXTABLE_LAMBDA), 4999., 0.02);
    cpl_test_abs(cpl_table_get_column_max(pt2x->table, MUSE_PIXTABLE_LAMBDA), 5001., 0.025);
    muse_pixtable_delete(pt2x);
    /* test failure cases of muse_pixtable_load_restricted_wavelength() */
    ps = cpl_errorstate_get();
    pt2x = muse_pixtable_load_restricted_wavelength(NULL, 4000., 10000.);
    cpl_test(!cpl_errorstate_is_equal(ps) &&
             cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
    cpl_errorstate_set(ps);
    /* don't know how to test failure of muse_pixtable_restrict_wavelength() */
    cpl_test_null(pt2x);
    pt2x = muse_pixtable_load_restricted_wavelength(fn, 5001., 4999.);
    cpl_test(!cpl_errorstate_is_equal(ps) && /* backwards range */
             cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND);
    cpl_errorstate_set(ps);
    cpl_test_null(pt2x);
    cpl_test_zero(unsetenv("MUSE_PIXTABLE_SAVE_AS_TABLE"));

    /* test saving and re-loading as image */
    char *fn3 = cpl_sprintf("%s/%s", dir, "muse_pixtable_as_image.fits");
    ps = cpl_errorstate_get();
    rc = muse_pixtable_save(pt, fn3);
    cpl_test(cpl_errorstate_is_equal(ps));
    cpl_test_zero(stat(fn3, &sb));
    cpl_test(S_ISREG(sb.st_mode) && sb.st_size > 0);
    if (i == 0) {
      /* ... and contain seven (image) extensions, and no flat-field */
      cpl_test(cpl_fits_count_extensions(fn3) == 7);
      cpl_test_zero(cpl_fits_find_extension(fn3, MUSE_PIXTABLE_FF_EXT));
    } else {
      /* ... and contain seven (image) extensions plus one flat-field table */
      cpl_test(cpl_fits_count_extensions(fn3) == 8);
      cpl_test_eq(cpl_fits_find_extension(fn3, MUSE_PIXTABLE_FF_EXT), 8);
    }
    ps = cpl_errorstate_get();
    muse_pixtable *pt3 = muse_pixtable_load(fn3);
    cpl_test(cpl_errorstate_is_equal(ps));
    cpl_test_nonnull(pt3);
    muse_test_pixtable_compare(pt, pt3);
    /* test muse_pixtable_restrict_wavelength() directly */
    rc = muse_pixtable_restrict_wavelength(pt3, 4950., 5050.);
    cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
    cpl_test(cpl_table_get_column_min(pt3->table, MUSE_PIXTABLE_LAMBDA) >= 4950.);
    cpl_test(cpl_table_get_column_max(pt3->table, MUSE_PIXTABLE_LAMBDA) <= 5050.);
    if (pt3->ffspec) {
      cpl_test(cpl_table_get_column_min(pt3->table, MUSE_PIXTABLE_FFLAMBDA) >= 4950. - 2.5);
      cpl_test(cpl_table_get_column_max(pt3->table, MUSE_PIXTABLE_FFLAMBDA) <= 5050. + 2.5);
    }
    if (i == 0) { /* failure of muse_pixtable_restrict_wavelength(), only once */
      ps = cpl_errorstate_get();
      rc = muse_pixtable_restrict_wavelength(NULL, 4000., 10000.);
      cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
      cpl_errorstate_set(ps);
      cpl_table_delete(pt3->table);
      pt3->table = NULL;
      rc = muse_pixtable_restrict_wavelength(pt3, 4000., 10000.);
      cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
      cpl_errorstate_set(ps);
    }
    muse_pixtable_delete(pt3);
    /* should still detect it without env. var set */
    pt3 = muse_pixtable_load(fn3);
    cpl_test(cpl_errorstate_is_equal(ps));
    cpl_test_nonnull(pt3);
    muse_test_pixtable_compare(pt, pt3);
    muse_pixtable_delete(pt3);
    /* test muse_pixtable_load_window(), should work equally well on both formats */
    muse_pixtable *ptcut = muse_pixtable_duplicate(pt),
                  *ptw0 = muse_pixtable_load_window(fn, 10, 12),
                  *ptw3 = muse_pixtable_load_window(fn3, 10, 12);
    /* cut part of the original table */
    cpl_table_erase_window(ptcut->table, 10+12, cpl_table_get_nrow(ptcut->table));
    cpl_table_erase_window(ptcut->table, 0, 10);
    muse_test_pixtable_compare(ptcut, ptw3); /* one should be equal to part of original */
    muse_test_pixtable_compare(ptw0, ptw3); /* both should be the same */
    muse_pixtable_delete(ptcut);
    muse_pixtable_delete(ptw0);
    muse_pixtable_delete(ptw3);
    cpl_test_zero(remove(fn));
    cpl_test_zero(remove(fn3));
    cpl_free(fn3);
    /* handle pixel table with bad ffspec extension */
    if (i == 0) {
      /* in the 1st iteration (no ffspec) create it */
      muse_pixtable_save(pt, fnbad);
      /* and add a bad (= image) extension with the right name for ffspec */
      cpl_image *image = cpl_image_new(2, 2, CPL_TYPE_DOUBLE);
      cpl_propertylist *himage = cpl_propertylist_new();
      cpl_propertylist_append_string(himage, "EXTNAME", MUSE_PIXTABLE_FF_EXT);
      cpl_image_save(image, fnbad, CPL_TYPE_UNSPECIFIED, himage, CPL_IO_EXTEND);
      cpl_propertylist_delete(himage);
      cpl_image_delete(image);
    } else {
      /* in the 2nd iteration (w/ffspec) try to load it */
      ps = cpl_errorstate_get();
      muse_pixtable *ptbad = muse_pixtable_load(fnbad);
      /* should succeed but for watch for the warning! */
      cpl_test_nonnull(ptbad);
      cpl_test(cpl_errorstate_is_equal(ps));
      cpl_errorstate_set(ps);
      muse_pixtable_delete(ptbad);
    }
    /* test failure cases of muse_pixtable_save() */
    ps = cpl_errorstate_get();
    rc = muse_pixtable_save(NULL, fn);
    cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
    cpl_errorstate_set(ps);
    rc = muse_pixtable_save(pt2, NULL);
    cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NULL_INPUT);
    cpl_errorstate_set(ps);
    cpl_test(cpl_errorstate_is_equal(ps));
    rc = muse_pixtable_save(pt2, "/pt.fits"); /* somewhere unaccessible */
    cpl_test(!cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_FILE_IO);
    cpl_errorstate_set(ps);
    muse_pixtable_delete(pt2);
    cpl_free(fn);
    /* test failure cases of muse_pixtable_load() */
    ps = cpl_errorstate_get();
    pt2 = muse_pixtable_load(NULL);
    cpl_test(!cpl_errorstate_is_equal(ps) &&
             cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
    cpl_errorstate_set(ps);
    cpl_test_null(pt2);
    pt2 = muse_pixtable_load("/bla_fail/pt2.fits"); /* somewhere inaccessible */
    cpl_test(!cpl_errorstate_is_equal(ps) &&
             cpl_error_get_code() == CPL_ERROR_FILE_IO);
    cpl_errorstate_set(ps);
    cpl_test_null(pt2);
  } /* for i (without and with flat-field spectrum) */
  cpl_test_zero(remove(fnbad));
  cpl_free(fnbad);

  /* test muse_pixtable_load_merge_channels() */
  /* start with a simple failure case */
  ps = cpl_errorstate_get();
  muse_pixtable *ptmerged = muse_pixtable_load_merge_channels(NULL, 4000., 10000.);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_test_null(ptmerged);
  cpl_errorstate_set(ps);
  /* now construct an exposure table and save small dummy pixel tables *
   * again, do these tests twice:                                      *
   *   i=0 means pixel table without flat-field spectrum               *
   *   i=1 means pixel table /with/ flat-field spectrum attached       */
  for (i = 0; i <= 1; i++) {
    cpl_table *exptable = muse_test_pixtable_create_exptable(dir,
                                                             i ? ffspec2 : NULL);
    cpl_size irow;
    for (irow = 0; irow < cpl_table_get_nrow(exptable); irow++) {
      cpl_table *et = cpl_table_extract(exptable, irow, 1);
      if (irow == 0) {
        cpl_table_erase_column(et, "00");
      }
      ps = cpl_errorstate_get();
      cpl_msg_debug(__func__, "____________________ merge irow == %d __________"
                    "__________ (%s ffspec) ____________________", (int)irow,
                    i == 1 ? "with" : "without");
      ptmerged = muse_pixtable_load_merge_channels(et, 4000., 10000.);
      if (irow != 2) { /* this was is missing on purpose! */
        cpl_test(cpl_errorstate_is_equal(ps));
        cpl_test_nonnull(ptmerged);
        if (irow == 0) { /* all IFUs there */
          cpl_test(muse_pixtable_get_nrow(ptmerged) == 10 * 24);
        } else if (irow == 1) { /* two missing ones */
          cpl_test(muse_pixtable_get_nrow(ptmerged) == 10 * 22);
        } else if (irow == 3) { /* was "merged" */
          cpl_test(muse_pixtable_get_nrow(ptmerged) == 10);
        }
        /* all wavelengths should be left */
        cpl_test_abs(cpl_table_get_column_min(ptmerged->table, MUSE_PIXTABLE_LAMBDA),
                     4800., DBL_EPSILON);
        cpl_test_abs(cpl_table_get_column_max(ptmerged->table, MUSE_PIXTABLE_LAMBDA),
                     5500., DBL_EPSILON);
        if (i == 1) { /* tests with ffspec */
          cpl_test_nonnull(ptmerged->ffspec);
          /* the mean of the merged spectrum should be the original mean      *
           * times a factor due to the construction of the individual spectra *
           * (exposure 2 has two missing pixel tables)                        */
          double factor = irow == 1 ? (20*1. + 1.3 + 0.9) / 22.
                                    : (22*1. + 1.3 + 0.9) / 24.;
          cpl_test_abs(cpl_table_get_column_mean(ptmerged->ffspec, MUSE_PIXTABLE_FFDATA),
                       factor * cpl_table_get_column_mean(ffspec2, MUSE_PIXTABLE_FFDATA),
                       200. * DBL_EPSILON);
          /* should contain the full wavelength range */
          cpl_test(cpl_table_get_column_min(ptmerged->ffspec,
                                            MUSE_PIXTABLE_FFLAMBDA) < 4300.);
          cpl_test(cpl_table_get_column_max(ptmerged->ffspec,
                                            MUSE_PIXTABLE_FFLAMBDA) > 9700.);
          cpl_test(cpl_propertylist_has(ptmerged->header, MUSE_HDR_PT_FFCORR));
          cpl_test_eq(cpl_propertylist_get_int(ptmerged->header, MUSE_HDR_PT_FFCORR),
                      irow == 1 ? 22 : 24);
        }
        muse_pixtable_delete(ptmerged);
      } else {
        cpl_test(!cpl_errorstate_is_equal(ps)
                 && cpl_error_get_code() == CPL_ERROR_FILE_NOT_FOUND);
        cpl_test_null(ptmerged);
        cpl_errorstate_set(ps);
      }
      /* with with restricted range, only 3 of 10 input rows should be left */
      muse_pixtable *ptmergedx = muse_pixtable_load_merge_channels(et, 4999., 5001.);
      if (irow != 2) { /* this was is missing on purpose! */
        cpl_test(cpl_errorstate_is_equal(ps));
        cpl_test_nonnull(ptmergedx);
        if (irow == 0) { /* all IFUs there */
          cpl_test(muse_pixtable_get_nrow(ptmergedx) == 3 * 24);
        } else if (irow == 1) { /* two missing ones */
          cpl_test(muse_pixtable_get_nrow(ptmergedx) == 3 * 22);
        } else if (irow == 3) { /* was "merged" */
          cpl_test(muse_pixtable_get_nrow(ptmergedx) == 3);
        }
        /* only wavelengths of 5000. Angstrom should be left */
        cpl_test_abs(cpl_table_get_column_min(ptmergedx->table, MUSE_PIXTABLE_LAMBDA),
                     5000., DBL_EPSILON);
        cpl_test_abs(cpl_table_get_column_max(ptmergedx->table, MUSE_PIXTABLE_LAMBDA),
                     5000., DBL_EPSILON);
        if (i == 1) {
          /* also test the ffspec length, by should be 2.5 Angstrom wider,  *
           * but since sampling is in 1.25 steps around 5000.0, just 1.5... */
          cpl_test_abs(cpl_table_get_column_min(ptmergedx->ffspec,
                                                MUSE_PIXTABLE_FFLAMBDA),
                       5000. - 2.5, 1.25);
          cpl_test_abs(cpl_table_get_column_max(ptmergedx->ffspec,
                                                MUSE_PIXTABLE_FFLAMBDA),
                       5000. + 2.5, 1.25);
          cpl_test(cpl_propertylist_has(ptmergedx->header, MUSE_HDR_PT_FFCORR));
          cpl_test_eq(cpl_propertylist_get_int(ptmergedx->header, MUSE_HDR_PT_FFCORR),
                      irow == 1 ? 22 : 24);
        }
        muse_pixtable_delete(ptmergedx);
      } else {
        cpl_test(!cpl_errorstate_is_equal(ps)
                 && cpl_error_get_code() == CPL_ERROR_FILE_NOT_FOUND);
        cpl_test_null(ptmergedx);
        cpl_errorstate_set(ps);
      }
      cpl_table_delete(et);
    } /* for irow (exposure table rows) */

    /* for ffspec-case also try with one missing ffspec */
    fnbad = cpl_sprintf("%s/pt_0001-10_bad.fits", dir);
    if (i == 1) {
      cpl_table *et = cpl_table_extract(exptable, 0, 1);
      cpl_table_set_string(et, "10", 0, fnbad);
      ps = cpl_errorstate_get();
      ptmerged = muse_pixtable_load_merge_channels(et, 4000., 10000.);
      cpl_test(!cpl_errorstate_is_equal(ps)
               && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
      cpl_test_null(ptmerged);
      cpl_errorstate_set(ps);
      cpl_table_delete(et);
    } /* if i == 1 */

    /* now delete the table, the respective files, and the extra bad file */
    muse_test_pixtable_delete_exptable(exptable);
    if (i == 1) {
      /* delete extra file */
      cpl_test_zero(remove(fnbad));
    }
    cpl_free(fnbad);
  } /* for i (without and with flat-field spectrum) */
  cpl_table_delete(ffspec2);

  /* pseudo-test for muse_pixtable_dump(), check the output that it worked... */
  cpl_msg_debug(__func__, "muse_pixtable_dump() with header with units");
  muse_pixtable_dump(pt, 0, 10, 1);
  cpl_msg_debug(__func__, "muse_pixtable_dump() with header without units");
  muse_pixtable_dump(pt, 0, 10, 2);
  cpl_msg_debug(__func__, "muse_pixtable_dump() without header");
  muse_pixtable_dump(pt, 0, 10, 0);
  ps = cpl_errorstate_get();
  muse_pixtable_dump(NULL, 0, 10, 0);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  muse_pixtable_dump(pt, -5, 10, 0);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  muse_pixtable_dump(pt, 2, -5, 0);
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  cpl_table_erase_column(pt->table, MUSE_PIXTABLE_LAMBDA);
  muse_pixtable_dump(pt, 0, 10, 0); /* column missing */
  cpl_test(!cpl_errorstate_is_equal(ps) &&
           cpl_error_get_code() == CPL_ERROR_BAD_FILE_FORMAT);
  cpl_errorstate_set(ps);

  /* test muse_pixtable_is_fluxcal(), muse_pixtable_is_skysub(), *
   * and muse_pixtable_is_rvcorr()                               */
  ps = cpl_errorstate_get();
  cpl_boolean fluxcal = muse_pixtable_is_fluxcal(pt);
  cpl_test(!fluxcal); /* set up as not flux-calibrated */
  cpl_boolean skysub = muse_pixtable_is_skysub(pt);
  cpl_test(!skysub); /* set up as not sky subtracted */
  cpl_test(cpl_errorstate_is_equal(ps));
  cpl_boolean rvcorr = muse_pixtable_is_rvcorr(pt);
  cpl_test(!rvcorr); /* set up as not RV corrected */
  muse_pixtable *ptdupe = muse_pixtable_duplicate(pt);
  cpl_propertylist_append_double(ptdupe->header, MUSE_HDR_PT_RVCORR, 10.);
  rvcorr = muse_pixtable_is_rvcorr(ptdupe);
  cpl_test(rvcorr); /* set up as RV corrected */
  muse_pixtable_delete(ptdupe);
  /* failure cases */
  ps = cpl_errorstate_get();
  fluxcal = muse_pixtable_is_fluxcal(NULL);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  skysub = muse_pixtable_is_skysub(NULL);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  rvcorr = muse_pixtable_is_rvcorr(NULL);
  cpl_test(!cpl_errorstate_is_equal(ps) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  /* success cases are tested in muse_test_flux and sky_master, respectively */

  /* test muse_pixtable_delete() */
  ps = cpl_errorstate_get();
  muse_pixtable_delete(NULL); /* "failure" case */
  cpl_test(cpl_errorstate_is_equal(ps));
  muse_pixtable_delete(pt); /* normal case */
  cpl_test(cpl_errorstate_is_equal(ps));

  /* test muse_pixtable_to_imagelist() and muse_pixtable_from_imagelist(); *
   * for these tests, start with the original input data again             */
  pt = muse_pixtable_duplicate(ptkeep); /* duplicate for the work */
  ps = cpl_errorstate_get();
  muse_imagelist *images = muse_pixtable_to_imagelist(pt);
  cpl_test(cpl_errorstate_is_equal(ps));
  cpl_test(muse_imagelist_get_size(images) == 1);
  /* cross check valid datapoints with the original input image */
  inimage = muse_image_load(BASEFILENAME"_create_data.fits");
  nx = cpl_image_get_size_x(inimage->data);
  ny = cpl_image_get_size_y(inimage->data);
  float *indata = cpl_image_get_data_float(inimage->data),
        *instat = cpl_image_get_data_float(inimage->stat),
        *outdata = cpl_image_get_data_float(muse_imagelist_get(images, 0)->data),
        *outstat = cpl_image_get_data_float(muse_imagelist_get(images, 0)->stat);
  int *indq = cpl_image_get_data_int(inimage->dq),
      *outdq = cpl_image_get_data_int(muse_imagelist_get(images, 0)->dq);
  int j, nmissing = 0;
  cpl_boolean ok = CPL_TRUE;
  for (i = 0; i < nx; i++) {
    for (j = 0; j < ny; j++) {
      cpl_size pos = i + j*nx;
      if (outdq[pos] & EURO3D_MISSDATA) {
        nmissing++;
        continue; /* region outside a slice, this will be different! */
      }
      /* all other pixels should be identical */
      if ((indq[pos] != outdq[pos]) ||
          (fabs(indata[pos] - outdata[pos]) > FLT_EPSILON) ||
          (fabs(instat[pos] - outstat[pos]) > FLT_EPSILON)) {
        ok = CPL_FALSE;
      } /* if */
    } /* for j */
  } /* for i */
  muse_image_delete(inimage);
  cpl_test(ok);
  cpl_msg_debug(__func__, "found %d pixels outside slices", nmissing);
  /* should be at least 6 x 49 x 4112 = 1.2e6 pixels outside slices,   *
   * but no more than 20% of the whole CCD (4096 x 4112 * 0.2 = 3.3e6) */
  cpl_test(nmissing > 1.2e6 && nmissing < 3.3e6);
  /* check that the slice centers are all there and in increasing order */
  float oldcenter = 0;
  int nslice;
  for (nslice = 1; nslice <= kMuseSlicesPerCCD; nslice++) {
    char *kw = cpl_sprintf("ESO DRS MUSE SLICE%0d CENTER", nslice);
    float center = cpl_propertylist_get_float(muse_imagelist_get(images, 0)->header,
                                              kw);
    cpl_test(center > oldcenter);
    cpl_free(kw);
    oldcenter = center;
  } /* for nslice */
  /* fill the data and stat columns with some values *
   * before, to check that they get overwritten      */
  cpl_size nrow = muse_pixtable_get_nrow(pt);
  cpl_table_fill_column_window(pt->table, MUSE_PIXTABLE_DATA, 0, nrow, -999.);
  cpl_table_fill_column_window(pt->table, MUSE_PIXTABLE_STAT, 0, nrow, 999e9);
  ps = cpl_errorstate_get();
  rc = muse_pixtable_from_imagelist(pt, images);
  cpl_test(cpl_errorstate_is_equal(ps) && rc == CPL_ERROR_NONE);
  muse_imagelist_delete(images);
  ok = CPL_TRUE;
  cpl_size irow;
  for (irow = 0; irow < nrow; irow++) {
    double ddiff = fabs(cpl_table_get_float(pt->table, MUSE_PIXTABLE_DATA, irow, NULL)
                        - cpl_table_get_float(ptkeep->table, MUSE_PIXTABLE_DATA, irow, NULL)),
           sdiff = fabs(cpl_table_get_float(pt->table, MUSE_PIXTABLE_STAT, irow, NULL)
                        - cpl_table_get_float(ptkeep->table, MUSE_PIXTABLE_STAT, irow, NULL));
    if (ddiff > FLT_EPSILON || sdiff > FLT_EPSILON) {
      ok = CPL_FALSE;
    } /* if bad comparison */
  } /* for irow */
  cpl_test(ok);
  muse_pixtable_delete(pt);

  /* test failure cases that are not otherwise covered */
  muse_pixtable *ptkeep2 = muse_pixtable_duplicate(ptkeep);
  cpl_table_erase_column(ptkeep2->table, MUSE_PIXTABLE_XPOS);
  cpl_test(muse_pixtable_wcs_check(ptkeep2) == MUSE_PIXTABLE_WCS_UNKNOWN);
  cpl_test(!cpl_errorstate_is_equal(ps) /* inexisting column */
           && cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(ps);
  muse_pixtable_delete(ptkeep2);
  ptkeep2 = muse_pixtable_duplicate(ptkeep);
  cpl_table_erase_column(ptkeep2->table, MUSE_PIXTABLE_YPOS);
  cpl_test(muse_pixtable_wcs_check(ptkeep2) == MUSE_PIXTABLE_WCS_UNKNOWN);
  cpl_test(!cpl_errorstate_is_equal(ps) /* inexisting column */
           && cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(ps);
  muse_pixtable_delete(ptkeep2);
  cpl_test(muse_pixtable_wcs_check(ptkeep) == MUSE_PIXTABLE_WCS_PIXEL);
  cpl_table_set_column_unit(ptkeep->table, MUSE_PIXTABLE_XPOS, "bla");
  ps = cpl_errorstate_get();
  cpl_test(muse_pixtable_wcs_check(ptkeep) == MUSE_PIXTABLE_WCS_UNKNOWN);
  cpl_test(!cpl_errorstate_is_equal(ps) /* different units */
           && cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(ps);
  cpl_table_set_column_unit(ptkeep->table, MUSE_PIXTABLE_YPOS, "bla");
  cpl_test(muse_pixtable_wcs_check(ptkeep) == MUSE_PIXTABLE_WCS_UNKNOWN);
  cpl_test(!cpl_errorstate_is_equal(ps)
           && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(ps);
  cpl_propertylist_erase(ptkeep->header, MUSE_HDR_PT_TYPE);
  cpl_test(muse_pixtable_get_type(ptkeep) == MUSE_PIXTABLE_TYPE_UNKNOWN);
  cpl_test(!cpl_errorstate_is_equal(ps)
           && cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(ps);
  muse_pixtable_delete(ptkeep);

  /* muse_pixtable_extracted_*() are already well covered implicitly by  *
   * the muse_test_resampling program, and muse_pixtable_to_imagelist(), *
   * so just test failure cases here                                     */
  ps = cpl_errorstate_get();
  muse_pixtable **pts = muse_pixtable_extracted_get_slices(NULL);
  cpl_test(!cpl_errorstate_is_equal(ps)
           && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  cpl_test_null(pts);
  muse_pixtable_extracted_get_size(NULL);
  cpl_test(!cpl_errorstate_is_equal(ps)
           && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(ps);
  muse_pixtable_extracted_delete(NULL);
  cpl_test(cpl_errorstate_is_equal(ps));

  /* test muse_pixtable_erase_ifu_slice() */
  muse_pixtable *ptsmall = cpl_calloc(1, sizeof(muse_pixtable));
  ptsmall->table = muse_cpltable_new(muse_pixtable_def, PTLEN);
  cpl_table_fill_column_window_float(ptsmall->table, MUSE_PIXTABLE_XPOS, 0, PTLEN, 0.);
  cpl_table_fill_column_window_float(ptsmall->table, MUSE_PIXTABLE_YPOS, 0, PTLEN, 0.);
  cpl_table_fill_column_window_float(ptsmall->table, MUSE_PIXTABLE_LAMBDA, 0, PTLEN, 0.);
  cpl_table_fill_column_window_float(ptsmall->table, MUSE_PIXTABLE_DATA, 0, PTLEN, 0.);
  cpl_table_fill_column_window_int(ptsmall->table, MUSE_PIXTABLE_DQ, 0, PTLEN, 0.);
  cpl_table_fill_column_window_float(ptsmall->table, MUSE_PIXTABLE_STAT, 0, PTLEN, 0.);
  ptsmall->header = cpl_propertylist_new(); /* needed for dump */
  int ir;
  for (ir = 0; ir < PTLEN; ir++) {
    ifu = ir < 4 ? 1 : ir < 8 ? 3 : 5;
    slice = ir == 6 ? ir : ir + 1; /* get two IFU 3 / Slice 6 */
    cpl_table_set_int(ptsmall->table, MUSE_PIXTABLE_ORIGIN, ir,
                      muse_pixtable_origin_encode(1, 1, ifu, slice, 100));
  } /* for ir */
  cpl_errorstate pstate = cpl_errorstate_get();
#if 0
  printf("ptsmall original:\n");
  muse_pixtable_dump(ptsmall, 0, 100, 1);
  fflush(stdout);
  cpl_errorstate_set(pstate); /* swallow missing-header-info errors */
#endif
  int norig = muse_pixtable_get_nrow(ptsmall);
  pstate = cpl_errorstate_get();
  cpl_error_code ec = muse_pixtable_erase_ifu_slice(ptsmall, 1, 1);
  cpl_test(ec == CPL_ERROR_NONE && cpl_errorstate_is_equal(pstate));
  cpl_test_eq(muse_pixtable_get_nrow(ptsmall), norig - 1); /* one row less */
#if 0
  pstate = cpl_errorstate_get();
  printf("ptsmall erased 1/1:\n");
  muse_pixtable_dump(ptsmall, 0, 100, 1);
  fflush(stdout);
  cpl_errorstate_set(pstate); /* swallow missing-header-info errors */
#endif
  /* should work when the given IFU/slice is not there */
  pstate = cpl_errorstate_get();
  ec = muse_pixtable_erase_ifu_slice(ptsmall, 2, 1);
  cpl_test(ec == CPL_ERROR_NONE && cpl_errorstate_is_equal(pstate));
  ec = muse_pixtable_erase_ifu_slice(ptsmall, 6, 10);
  cpl_test(ec == CPL_ERROR_NONE && cpl_errorstate_is_equal(pstate));
  cpl_test_eq(muse_pixtable_get_nrow(ptsmall), norig - 1); /* still the same */
  /* should delete all entries for a slice */
  pstate = cpl_errorstate_get();
  ec = muse_pixtable_erase_ifu_slice(ptsmall, 3, 6);
  cpl_test(ec == CPL_ERROR_NONE && cpl_errorstate_is_equal(pstate));
  cpl_test_eq(muse_pixtable_get_nrow(ptsmall), norig - 3); /* now 3 less */
#if 0
  pstate = cpl_errorstate_get();
  printf("ptsmall erased 3/6:\n");
  muse_pixtable_dump(ptsmall, 0, 100, 1);
  fflush(stdout);
  cpl_errorstate_set(pstate); /* swallow missing-header-info errors */
#endif
  /* failure cases */
  pstate = cpl_errorstate_get();
  ec = muse_pixtable_erase_ifu_slice(NULL, 5, 9);
  cpl_test(ec == CPL_ERROR_NULL_INPUT && !cpl_errorstate_is_equal(pstate));
  cpl_errorstate_set(pstate);
  cpl_table_select_all(ptsmall->table);
  cpl_table_erase_selected(ptsmall->table);
  ec = muse_pixtable_erase_ifu_slice(ptsmall, 5, 9); /* no rows */
  cpl_test(ec == CPL_ERROR_DATA_NOT_FOUND && !cpl_errorstate_is_equal(pstate));
  cpl_errorstate_set(pstate);
  muse_pixtable_delete(ptsmall);

  /* also remove the temporary dir */
  cpl_test_zero(rmdir(dir));

  return cpl_test_end(0);
}
