/* -*- 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-2015 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 _DEFAULT_SOURCE
#define _BSD_SOURCE /* get setenv() from stdlib.h */
#include <stdlib.h> /* setenv() */
#include <string.h>

#include <muse.h>

/* accuracy limits for xsc, ysc, xang, yang recovered from INM test data */
const double klimitsINM[][4] = {
  { 5.3e-8, 2.3e-8, 0.012, 0.020 }, /* for Gaussian */
  { 4.8e-8, 2.9e-8, 0.009, 0.009 }, /* for Moffat */
  { 4.8e-8, 2.4e-8, 0.001, 0.020 }  /* for Box */
};
const double kLimitPos = 0.05; /* [pix] object position detection accuracy */
/* accuracy limits for coordinate transformation tests */
const double kLimitDeg = DBL_EPSILON * 115; /* ~10 nano-arcsec for trafo to deg */
const double kLimitDegF = FLT_EPSILON * 13.51; /* ~5.8 milli-arcsec for trafo to  *
                                                * deg, with value stored in float */
const double kLimitPix = FLT_EPSILON * 205; /* ~1/40000th pixel for trafo to pix */
const double kLimitRot = FLT_EPSILON * 10; /* ~4.3 milli-arcsec for rotations */
const double kLimitSca = DBL_EPSILON * 7461; /* pixel scales to ~5.5 nano-arcsec */
const double kLimitPPl = FLT_EPSILON * 2.88; /* ~1.24 milli-arcsec in proj. plane */

/* test coordinates in the pixel plane */
const int kPx[][4] = {
  {   1,   1 }, /* increasing x position */
  {  50,   1 },
  { 100,   1 },
  { 150,   1 },
  { 200,   1 },
  { 250,   1 },
  { 300,   1 },
  {   1,  50 }, /* increasing y position */
  {   1, 100 },
  {   1, 150 },
  {   1, 200 },
  {   1, 250 },
  {   1, 300 },
  {  50,  50 }, /* diagonal */
  { 100, 100 },
  { 150, 150 },
  { 200, 200 },
  { 250, 250 },
  { 300, 300 },
  {  -1,  -1 }
};


/* check that detected objects are close to the expected position, *
 * assume that the objects are relatively isolated (no others      *
 * within the surrounding +/- 5 pix)                               */
void
muse_test_wcs_check_object_detection(cpl_table *aDetected, double aX, double aY)
{
  int i, nrow = cpl_table_get_nrow(aDetected);
  for (i = 0; i < nrow; i++) {
    double x = cpl_table_get_double(aDetected, "XPOS", i, NULL),
           y = cpl_table_get_double(aDetected, "YPOS", i, NULL),
           f = cpl_table_get_double(aDetected, "FLUX", i, NULL);
    if (fabs(aX - x) < 5 && fabs(aY - y) < 5) { /* right object */
      cpl_msg_debug(__func__, "expected at %f,%f, detected at %f,%f, deltas "
                    "are %e,%e, flux = %e", aX, aY, x, y, x - aX, y - aY, f);
      /* adapt check accuracy based on flux */
      double limit = kLimitPos;
      if (f < 500.) {
        limit *= 5.;
      } else if (f < 1000.) {
        limit *= 2.5;
      } else if (f < 10000.) {
        limit *= 2.4;
      } else if (f < 15000.) {
        limit *= 2.3;
      }
      cpl_test(fabs(aX - x) < limit && fabs(aY - y) < limit);
      return;
    }
  } /* for i (table rows) */
  /* this object was not found, fail the test! */
  cpl_test(CPL_ERROR_NONE == CPL_ERROR_DATA_NOT_FOUND);
} /* muse_test_wcs_check_object_detection() */

/* create typical MUSE header with TAN projection, *
 * similar to muse_wcs_create_default()            */
cpl_propertylist *
muse_test_wcs_create_typical_tan_header(void)
{
  cpl_propertylist *p = cpl_propertylist_new();
  cpl_propertylist_append_double(p, "CRPIX1", 156.);
  cpl_propertylist_append_double(p, "CRVAL1", 10.);
  cpl_propertylist_append_double(p, "RA", 10.);
  cpl_propertylist_append_double(p, "CD1_1", -0.2 / 3600.);
  cpl_propertylist_append_string(p, "CTYPE1", "RA---TAN");
  cpl_propertylist_append_string(p, "CUNIT1", "deg");
  cpl_propertylist_append_double(p, "CRPIX2", 151.);
  cpl_propertylist_append_double(p, "CRVAL2", -30.);
  cpl_propertylist_append_double(p, "DEC", -30.);
  cpl_propertylist_append_double(p, "CD2_2", 0.2 / 3600);
  cpl_propertylist_append_string(p, "CTYPE2", "DEC--TAN");
  cpl_propertylist_append_string(p, "CUNIT2", "deg");
  cpl_propertylist_append_double(p, "CD1_2", 0.);
  cpl_propertylist_append_double(p, "CD2_1", 0.);
  return p;
} /* muse_test_wcs_create_typical_tan_header() */

/* create typical MUSE header with linear scaling, *
 * similar to muse_resampling_cube                 */
cpl_propertylist *
muse_test_wcs_create_typical_lin_header(void)
{
  cpl_propertylist *p = cpl_propertylist_new();
  cpl_propertylist_append_double(p, "CRPIX1", 1.);
  cpl_propertylist_append_double(p, "CRVAL1", -150.);
  cpl_propertylist_append_string(p, "CTYPE1", "PIXEL");
  cpl_propertylist_append_string(p, "CUNIT1", "pixel");
  cpl_propertylist_append_double(p, "CD1_1", 1.);
  cpl_propertylist_append_double(p, "CRPIX2", 1.);
  cpl_propertylist_append_double(p, "CRVAL2", -150.);
  cpl_propertylist_append_string(p, "CTYPE2", "PIXEL");
  cpl_propertylist_append_string(p, "CUNIT2", "pixel");
  cpl_propertylist_append_double(p, "CD2_2", 1.);
  cpl_propertylist_append_double(p, "CD1_2", 0.);
  cpl_propertylist_append_double(p, "CD2_1", 0.);
  /* RA,DEC needed to not trigger error states */
  cpl_propertylist_append_double(p, "RA", 10.);
  cpl_propertylist_append_double(p, "DEC", -30.);
  return p;
} /* muse_test_wcs_create_typical_lin_header() */

/* create header for part of example 1 (Table 5) of   *
 * Greisen & Calabretta 2002 A&A 395, 1077 (Paper II) */
cpl_propertylist *
muse_test_wcs_create_example_1_header(void)
{
  cpl_propertylist *p = cpl_propertylist_new();
  /* leave out the velocity and stokes axes */
  cpl_propertylist_append_int(p, "NAXIS", 2);
  cpl_propertylist_append_int(p, "NAXIS1", 512);
  cpl_propertylist_append_int(p, "NAXIS2", 512);
  cpl_propertylist_append_double(p, "CRPIX1", 256.);
  /* use the CDi_j matrix instead of CDELT */
  cpl_propertylist_append_double(p, "CD1_1", -0.003);
  cpl_propertylist_append_string(p, "CTYPE1", "RA---TAN");
  cpl_propertylist_append_double(p, "CRVAL1", 45.83);
  cpl_propertylist_append_string(p, "CUNIT1", "deg");
  cpl_propertylist_append_double(p, "CRPIX2", 257.);
  cpl_propertylist_append_double(p, "CD2_2", 0.003);
  cpl_propertylist_append_string(p, "CTYPE2", "DEC--TAN");
  cpl_propertylist_append_double(p, "CRVAL2", 63.57);
  cpl_propertylist_append_string(p, "CUNIT2", "deg");
  /* no cross terms */
  cpl_propertylist_append_double(p, "CD1_2", 0.);
  cpl_propertylist_append_double(p, "CD2_1", 0.);
  /* XXX RA,DEC identical to the CRVALi for our functions */
  cpl_propertylist_append_double(p, "RA", 45.83);
  cpl_propertylist_append_double(p, "DEC", 63.57);
  return p;
} /* muse_test_wcs_create_example_1_header() */

/* check muse_wcs transformations against reference values for the same *
 * header using the CPL wrapper around wcslib; aFudge is an extra fudge *
 * factor so that one can be more generous for some tests               */
void
muse_test_wcs_check_celestial_transformation(cpl_propertylist *aWCS,
                                             const char *aComment,
                                             const double aFudge)
{
  cpl_wcs *cwcs = cpl_wcs_new_from_propertylist(aWCS);
  int i = -1;
  while (kPx[++i][0] > 0) {
    /* compute reference data using the CPL wrapper around wcslib */
    cpl_matrix *pxcoords = cpl_matrix_new(1, 2);
    cpl_matrix_set(pxcoords, 0, 0, kPx[i][0]);
    cpl_matrix_set(pxcoords, 0, 1, kPx[i][1]);
    cpl_matrix *skycoords = NULL;
    cpl_array *status = NULL;
    cpl_wcs_convert(cwcs, pxcoords, &skycoords, &status, CPL_WCS_PHYS2WORLD);
    cpl_array_delete(status);
    cpl_matrix_delete(pxcoords);
    double raref = cpl_matrix_get(skycoords, 0, 0),
           decref = cpl_matrix_get(skycoords, 0, 1);
    cpl_matrix_delete(skycoords);

    /* projection onto the sky */
    double ra, dec;
    cpl_test(muse_wcs_celestial_from_pixel(aWCS, kPx[i][0], kPx[i][1], &ra, &dec)
             == CPL_ERROR_NONE);
    cpl_test(fabs(ra - raref) < aFudge * kLimitDeg);
    cpl_test(fabs(dec - decref) < aFudge * kLimitDeg);
    cpl_msg_debug(__func__, "%s: ra/dec = %f,%f (%f,%f, %e,%e <? %e)", aComment,
                  ra, dec, raref, decref, ra - raref, dec - decref,
                  aFudge * kLimitDeg);

    /* projection into the pixel plane */
    double x, y;
    cpl_test(muse_wcs_pixel_from_celestial(aWCS, ra, dec, &x, &y)
             == CPL_ERROR_NONE);
    cpl_test(fabs(x - kPx[i][0]) < kLimitPix);
    cpl_test(fabs(y - kPx[i][1]) < kLimitPix);
    cpl_msg_debug(__func__, "%s: x,y = %f,%f (%d,%d, %e,%e <? %e)", aComment,
                  x, y, kPx[i][0], kPx[i][1], x - kPx[i][0], y - kPx[i][1],
                  kLimitPix);
  } /* while */
  cpl_wcs_delete(cwcs);
} /* muse_test_wcs_check_celestial_transformation() */

#define PRINT_USAGE(rc)                                                        \
  fprintf(stderr, "\nUsage: %s [ -centroid <gaussian | moffat | box> ] "       \
          "[ -detsigma <float> ] [ -radius <float> ] [ -faccuracy <float> ] "  \
          "[ -niter <int> ] [ -rejsigma <float> ] [ -rotcenter <float>,<float ]"\
          " DATACUBE_ASTROMETRY ASTROMETRY_REFERENCE\n"                        \
          "  The output ASTROMETRY_WCS is written to a file starting with the" \
          " same basename as DATACUBE_ASTROMETRY.\n\n\n", argv[0]);            \
  return (rc);

int
muse_test_wcs_calibrate_from_cube(int argc, char **argv)
{
  cpl_errorstate state = cpl_errorstate_get();
  cpl_msg_set_level(CPL_MSG_DEBUG);

  /* defaults */
  char *incube = NULL,
       *intable = NULL;
  muse_wcs_centroid_type centroid = MUSE_WCS_CENTROID_MOFFAT;
  double detsigma = 1.5,
         radius = 3.,
         faccuracy = 0.,
         rejsigma = 3.,
         crpix1 = -0.01,
         crpix2 = -1.20;
  int niter = 2;

  /* argument processing */
  int i;
  for (i = 1; i < argc; i++) {
    if (strncmp(argv[i], "-centroid", 10) == 0) {
      /* skip to next arg to first slice number */
      i++;
      if (i < argc) {
        if (!strncmp(argv[i], "moffat", 7)) {
          centroid = MUSE_WCS_CENTROID_MOFFAT;
        } else if (!strncmp(argv[i], "gaussian", 9)) {
          centroid = MUSE_WCS_CENTROID_GAUSSIAN;
        } else if (!strncmp(argv[i], "box", 4)) {
          centroid = MUSE_WCS_CENTROID_BOX;
        }
      } else {
        PRINT_USAGE(2);
      }
    } else if (strncmp(argv[i], "-detsigma", 10) == 0) {
      /* skip to next arg to last slice number */
      i++;
      if (i < argc) {
        detsigma = atof(argv[i]);
      } else {
        PRINT_USAGE(3);
      }
    } else if (strncmp(argv[i], "-radius", 8) == 0) {
      /* skip to next arg to last slice number */
      i++;
      if (i < argc) {
        radius = atof(argv[i]);
      } else {
        PRINT_USAGE(4);
      }
    } else if (strncmp(argv[i], "-faccuracy", 11) == 0) {
      /* skip to next arg to last slice number */
      i++;
      if (i < argc) {
        faccuracy = atof(argv[i]);
        if (faccuracy < 0) {
          PRINT_USAGE(5);
        }
      } else {
        PRINT_USAGE(5);
      }
    } else if (strncmp(argv[i], "-niter", 7) == 0) {
      /* skip to next arg to last slice number */
      i++;
      if (i < argc) {
        niter = atoi(argv[i]);
      } else {
        PRINT_USAGE(6);
      }
    } else if (strncmp(argv[i], "-rejsigma", 10) == 0) {
      /* skip to next arg to last slice number */
      i++;
      if (i < argc) {
        rejsigma = atof(argv[i]);
      } else {
        PRINT_USAGE(7);
      }
    } else if (strncmp(argv[i], "-rotcenter", 11) == 0) {
      /* skip to next arg to last slice number */
      i++;
      if (i < argc) {
        cpl_array *a = muse_cplarray_new_from_delimited_string(argv[i], ",");
        crpix1 = cpl_array_get_string(a, 0) ? atof(cpl_array_get_string(a, 0))
                                            : crpix1;
        crpix2 = cpl_array_get_string(a, 1) ? atof(cpl_array_get_string(a, 1))
                                            : crpix2;
        cpl_array_delete(a);
      } else {
        PRINT_USAGE(8);
      }
    } else if (strncmp(argv[i], "-", 1) == 0) { /* unallowed options */
      PRINT_USAGE(9);
    } else {
      if (incube && intable) {
        break; /* we have the possible names, skip the rest */
      }
      if (!incube) {
        incube = argv[i]; /* set the name for the input cube */
      } else {
        intable = argv[i] /* set the name for the input reference table */;
      }
    }
  } /* for i (all arguments) */

  muse_datacube *cube = muse_datacube_load(incube);
  if (!cube) {
    PRINT_USAGE(10);
  }

  cpl_frame *refframe = cpl_frame_new();
  cpl_frame_set_filename(refframe, intable);
  cpl_table *reftable = muse_postproc_load_nearest(cube->header, refframe,
                                                   20., 60., NULL, NULL);
  cpl_frame_delete(refframe);

  /* get the image from the cube */
  cpl_errorstate es = cpl_errorstate_get();
  muse_image *detimage = NULL;
  const char *extname = NULL;
  for (i = 0; i < cpl_array_get_size(cube->recnames); i++) {
    const char *name = cpl_array_get_string(cube->recnames, i);
    cpl_msg_debug(__func__, "image extension \"%s\"", name);
    if (name && !strncmp(name, MUSE_WCS_DETIMAGE_EXTNAME,
                         strlen(MUSE_WCS_DETIMAGE_EXTNAME) + 1)) {
      detimage = muse_imagelist_get(cube->recimages, i);
    }
  } /* for i (all cube image extensions) */
  if (!detimage) {
    cpl_errorstate_set(es); /* swallow access beyond boundaries */
    /* fall back to white-light image */
    cpl_msg_warning(__func__, "No \"%s\" image in cube, trying to reconstruct "
                    "it.", MUSE_WCS_DETIMAGE_EXTNAME);
    /* code copied from muse_wcs_locate_sources() */
    int iplane, irefplane = cpl_imagelist_get_size(cube->data) / 2;
    muse_imagelist *list = muse_imagelist_new();
    unsigned int ilist = 0;
    for (iplane = irefplane - 1; iplane <= irefplane + 1; iplane++) {
      muse_image *image = muse_image_new();
      image->data = cpl_image_duplicate(cpl_imagelist_get(cube->data, iplane));
      cpl_image_reject_value(image->data, CPL_VALUE_NAN); /* mark NANs */
      image->dq = cpl_image_new_from_mask(cpl_image_get_bpm(image->data));
      image->stat = cpl_image_duplicate(cpl_imagelist_get(cube->stat, iplane));
      muse_imagelist_set(list, image, ilist++);
    } /* for iplane (planes around ref. wavelength) */
    detimage = muse_combine_median_create(list);
    /* fill existing empty header with the header of the *
     * cube, but without the third dimension in the WCS  */
    cpl_propertylist_copy_property_regexp(detimage->header, cube->header,
                                          "^C...*3$|^CD3_.$", 1);
    muse_imagelist_delete(list);
    if (!detimage) {
      cpl_table_delete(reftable);
      muse_datacube_delete(cube);
      PRINT_USAGE(12);
    } /* if recreating detimage failed */

    /* append it to cube object as well, it needs to end up as 2nd  *
     * reconstructed image for muse_wcs_optimize_solution() to work */
    if (!cube->recnames) {
      cube->recnames = cpl_array_new(1, CPL_TYPE_STRING);
      cpl_array_set_string(cube->recnames, 0, "dummy");
    }
    if (!cube->recimages) {
      cube->recimages = muse_imagelist_new();
      muse_imagelist_set(cube->recimages, muse_image_duplicate(detimage), 0);
    }
    cpl_array_set_size(cube->recnames, cpl_array_get_size(cube->recnames) + 1);
    cpl_array_set_string(cube->recnames, cpl_array_get_size(cube->recnames) - 1,
                         MUSE_WCS_DETIMAGE_EXTNAME);
    muse_imagelist_set(cube->recimages, detimage,
                       muse_imagelist_get_size(cube->recimages));
  } else {
    extname = muse_pfits_get_extname(detimage->header);
  } /* else detimage from cube */
  es = cpl_errorstate_get();
  if (extname) {
    cpl_msg_info(__func__, "Loaded image \"%s\" from cube \"%s\"", extname, incube);
  } else {
    cpl_errorstate_set(es); /* swallow data not found */
    cpl_msg_info(__func__, "Recreated detection image from cube \"%s\"", incube);
  }

  /* now we loaded everything, do the processing */
  muse_wcs_object *wcs = muse_wcs_object_new();
  /* hardcode defaults here as set in muse_astrometry.xml */
  wcs->crpix1 = crpix1;
  wcs->crpix2 = crpix2;
  cpl_propertylist_erase_regexp(cube->header, "ESO QC", 0);
  wcs->ra = muse_pfits_get_ra(cube->header);
  wcs->dec = muse_pfits_get_dec(cube->header);
  wcs->detected = muse_wcs_centroid_stars(detimage, fabsf((float)detsigma), centroid);
  if (!reftable) {
    muse_datacube_delete(cube);
    muse_wcs_object_delete(wcs);
    PRINT_USAGE(11);
  }
  /* use defaults from the muse_astrometry recipe */
  wcs->cube = cube;
  cpl_error_code rc = CPL_ERROR_NONE;
  if (detsigma < 0.) {
    rc = muse_wcs_optimize_solution(wcs, detsigma, centroid, reftable, radius,
                                    faccuracy, niter, rejsigma);
  } else {
    rc = muse_wcs_solve(wcs, reftable, radius, faccuracy, niter, rejsigma);
  }
  wcs->cube = NULL; /* delete it separately */

  /* prepare input filename for creation of output names */
  char *dot = strrchr(incube, '.');
  *dot = '\0'; /* replace the dot */
  /* if we computed a (valid) WCS or if the detection image was *
   * recreated here, save it to disk                            */
  if ((wcs->wcs && rc == CPL_ERROR_NONE) || !extname) {
    /* add solution to header of the detection image and save it in any case */
    muse_wcs_copy_keywords(wcs->wcs, detimage->header, 'T', "Test Solution");
    char *fn = cpl_sprintf("%s_%s.fits", incube, MUSE_WCS_DETIMAGE_EXTNAME);
    cpl_error_code rc2 = muse_image_save(detimage, fn);
    if (rc2 == CPL_ERROR_NONE) {
      cpl_msg_info(__func__, "Saved reconstructed detection image to \"%s\".", fn);
    } else {
      cpl_msg_error(__func__, "Failed to save reconstructed detection image to "
                    "\"%s\": %s", fn, cpl_error_get_message());
    }
    cpl_free(fn);
  } /* if wcs or no extname */

  if (wcs->wcs && rc == CPL_ERROR_NONE) {
    char *object = cpl_sprintf("Astrometric calibration (%s)",
                               cpl_propertylist_get_string(cube->header, "OBJECT"));
    cpl_propertylist_update_string(wcs->wcs, "OBJECT", object);
    char *fn = cpl_sprintf("%s_%s.fits", incube, MUSE_TAG_ASTROMETRY_WCS);
    cpl_error_code rc2 = cpl_propertylist_save(wcs->wcs, fn, CPL_IO_CREATE);
    /* save detections table separately to keep the WCS in the primary header */
    cpl_propertylist *header = cpl_propertylist_new();
    cpl_propertylist_append_string(header, "EXTNAME",
                                   MUSE_WCS_SOURCE_TABLE_NAME);
    cpl_error_code rc3 = cpl_table_save(wcs->detected, NULL, header, fn,
                                        CPL_IO_EXTEND);
    cpl_propertylist_delete(header);
    if (rc2 == CPL_ERROR_NONE && rc3 == CPL_ERROR_NONE) {
      cpl_msg_info(__func__, "Saved solution and table to \"%s\".", fn);
    } else {
      cpl_msg_error(__func__, "Failed saving solution with table to \"%s\": %s",
                    fn, cpl_error_get_message());
    }
    cpl_free(fn);
    cpl_free(object);
  } /* if (valid) WCS */
  muse_wcs_object_delete(wcs);
  cpl_table_delete(reftable);
  muse_datacube_delete(cube);

  if (!cpl_errorstate_is_equal(state)) {
    cpl_msg_error(__func__, "Failed some computation step: %s",
                  cpl_error_get_message());
    cpl_errorstate_dump(state, CPL_FALSE, cpl_errorstate_dump_one);
  }
  return cpl_errorstate_is_equal(state) ? CPL_ERROR_NONE : cpl_error_get_code();
} /* muse_test_wcs_calibrate_from_cube() */

/*----------------------------------------------------------------------------*/
/**
  @brief    Test program to check that all the functions of the muse_wcs module
            work correctly.

  This program explicitely tests
    muse_wcs_object_new
    muse_wcs_object_delete
    muse_wcs_centroid_stars
    muse_wcs_locate_sources
    muse_wcs_solve
    muse_wcs_optimize_solution
    muse_wcs_create_default
    muse_wcs_project_tan
    muse_wcs_position_celestial
    muse_wcs_apply_cd
    muse_wcs_celestial_from_pixel
    muse_wcs_pixel_from_celestial
    muse_wcs_projplane_from_celestial
    muse_wcs_projplane_from_pixel
    muse_wcs_pixel_from_projplane
    muse_wcs_get_angles
    muse_wcs_get_scales
    muse_wcs_new
    muse_wcs_copy_keywords
  It implicitely also tests
    muse_wcs_pixel_from_celestial_fast
    muse_wcs_celestial_from_pixel_fast
    muse_wcs_projplane_from_pixel_fast
    muse_wcs_pixel_from_projplane_fast
 */
/*----------------------------------------------------------------------------*/
int main(int argc, char **argv)
{
  cpl_error_code rc = CPL_ERROR_NONE;
  if (argc > 1) {
    /* If there are arguments, misuse this program as astrometry calibrator. */
    cpl_init(CPL_INIT_DEFAULT);
    rc = muse_test_wcs_calibrate_from_cube(argc, argv);
    cpl_end();
    return rc;
  }

  /* start as test program */
  cpl_test_init(PACKAGE_BUGREPORT, CPL_MSG_DEBUG);

  /*********************************************
   * check WCS creation from input pixel table *
   *********************************************/
  muse_pixtable *ptin = muse_pixtable_load(BASEFILENAME"_pt_in.fits");
  cpl_table *wcsref = cpl_table_load(BASEFILENAME"_scene.fits", 1, 1);
  cpl_test(ptin && wcsref);
  /* run once with muse_wcs_locate_sources() to create cube and image */
  muse_wcs_object *wcsobj = muse_wcs_object_new();
  cpl_test_nonnull(wcsobj);
  rc = muse_wcs_locate_sources(ptin, 3., MUSE_WCS_CENTROID_MOFFAT, wcsobj);
  cpl_test(rc == CPL_ERROR_NONE);
  cpl_test_nonnull(wcsobj->cube);
  cpl_test_nonnull(wcsobj->detected);
  /* check that the white-light image was saved (as the 1st image) */
  cpl_test(cpl_array_get_string(wcsobj->cube->recnames, 0) &&
           !strncmp(cpl_array_get_string(wcsobj->cube->recnames, 0),
                    "white", 6));
  /* check that the detection image was saved (as the 2nd image) */
  cpl_test(cpl_array_get_string(wcsobj->cube->recnames, 1) &&
           !strncmp(cpl_array_get_string(wcsobj->cube->recnames, 1),
                    MUSE_WCS_DETIMAGE_EXTNAME,
                    strlen(MUSE_WCS_DETIMAGE_EXTNAME) + 1));
  /* loop through the possible centroid types, use muse_wcs_centroid_stars() *
   * for this so that it doesn't need to recreate the whole cube every time  */
  muse_wcs_centroid_type type;
  const char *typestring[] = {
    "GAUSSIAN",
    "MOFFAT",
    "BOX"
  };
  for (type = MUSE_WCS_CENTROID_GAUSSIAN; type <= MUSE_WCS_CENTROID_BOX; type++) {
    cpl_msg_debug(__func__, "\n____ centroid type %s _____", typestring[type]);
    /* for the last one, replace the normal DAR *
     * header with the one from dar_check       */
    if (type == MUSE_WCS_CENTROID_BOX) {
      cpl_propertylist_erase_regexp(ptin->header, MUSE_HDR_PT_DAR_NAME, 0);
      cpl_propertylist_append_float(ptin->header, MUSE_HDR_PT_DAR_CORR, 0.001);
    }
    cpl_errorstate es = cpl_errorstate_get();
    muse_image *image = muse_imagelist_get(wcsobj->cube->recimages, 1);
    cpl_table *detected = muse_wcs_centroid_stars(image, 3., type);
    cpl_test(cpl_errorstate_is_equal(es));
    cpl_test_nonnull(detected);
#if 0
    char *fn = cpl_sprintf("muse_test_wcs_detected_%s.fits", typestring[type]);
    cpl_table_save(detected, NULL, NULL, fn, CPL_IO_CREATE);
    cpl_free(fn);
#endif
    cpl_test(cpl_table_get_nrow(detected) == 63);
    /* check position of selected isolated objects, reference *
     * positions measured using IRAF imexamine 'a'            */
    muse_test_wcs_check_object_detection(detected,  26.47,  41.08);
    muse_test_wcs_check_object_detection(detected,  62.98, 121.62);
    muse_test_wcs_check_object_detection(detected, 169.56,  33.6);
    muse_test_wcs_check_object_detection(detected, 117.26,  26.92);
    muse_test_wcs_check_object_detection(detected, 233.57,  41.53);
    muse_test_wcs_check_object_detection(detected, 211.7,  151.3);
    muse_test_wcs_check_object_detection(detected, 163.29, 152.43);
    muse_test_wcs_check_object_detection(detected,  90.38, 168.80);
    muse_test_wcs_check_object_detection(detected,  30.85, 253.93);
    muse_test_wcs_check_object_detection(detected, 187.42, 233.65);
    muse_test_wcs_check_object_detection(detected, 237.70, 270.73);
    muse_test_wcs_check_object_detection(detected, 226.48, 231.46);

    /* for one, also test with an image without dq component but with NANs */
    if (type == MUSE_WCS_CENTROID_GAUSSIAN) {
      muse_image *image2 = muse_image_duplicate(image);
      muse_image_dq_to_nan(image2);
      es = cpl_errorstate_get();
      cpl_table *detected2 = muse_wcs_centroid_stars(image2, 3., type);
      cpl_test(cpl_errorstate_is_equal(es));
      cpl_test_nonnull(detected);
      /* rerun selected tests */
      cpl_test(cpl_table_get_nrow(detected) == 63);
      muse_test_wcs_check_object_detection(detected,  26.47,  41.08);
      muse_test_wcs_check_object_detection(detected, 233.57,  41.53);
      muse_test_wcs_check_object_detection(detected,  30.85, 253.93);
      muse_image_delete(image2);
      cpl_table_delete(detected2);
    }

    /* see how to use this table further; for that replace it  *
     * into the WCS object, but keep the existing table around */
    cpl_table *saved = wcsobj->detected;
    wcsobj->detected = detected;
    /* remove the previous solution */
    cpl_propertylist_delete(wcsobj->wcs);
    wcsobj->wcs = NULL;
    rc = muse_wcs_solve(wcsobj, wcsref, 5., 5., 2, 3.);
    cpl_msg_debug(__func__, "Solving WCS returned rc=%d: %s", rc,
                  rc != CPL_ERROR_NONE ? cpl_error_get_message() : "");
    cpl_test(rc == CPL_ERROR_NONE);
    cpl_test_nonnull(wcsobj->wcs);
    cpl_test(cpl_table_has_column(wcsobj->detected, "RA"));
    cpl_test(cpl_table_has_column(wcsobj->detected, "DEC"));
    /* get output fit properties (using functions tested below...) */
    double xang, yang, xsc, ysc;
    muse_wcs_get_angles(wcsobj->wcs, &xang, &yang);
    muse_wcs_get_scales(wcsobj->wcs, &xsc, &ysc);
    cpl_msg_debug(__func__, "angles: %e,%e, scales: %e,%e", xang, yang, xsc, ysc);
    cpl_test_lt(fabs(xang), klimitsINM[type][2]); /* almost no rotation */
    cpl_test_lt(fabs(yang), klimitsINM[type][3]);
    /* should both be about 0.2''/px */
    cpl_test_abs(xsc, 0.2/3600., klimitsINM[type][0]);
    cpl_test_abs(ysc, 0.2/3600., klimitsINM[type][1]);
    /* test the added keywords */
    cpl_test(cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_RADIUS) > 0. &&
             cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_RADIUS) <= 5.);
    cpl_test(cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_ACCURACY) > 0.);
    cpl_test(cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_FACCURACY) > 0. &&
             cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_FACCURACY) <= 5.);

    /* also test the iterative function */
    if (type == MUSE_WCS_CENTROID_GAUSSIAN) {
      /* run it with extra debug dumps once */
      setenv("MUSE_DEBUG_WCS", "1", 1);
    }
    rc = muse_wcs_optimize_solution(wcsobj, -3., type, wcsref, 5., 5., 2, 3.);
    muse_wcs_get_angles(wcsobj->wcs, &xang, &yang);
    muse_wcs_get_scales(wcsobj->wcs, &xsc, &ysc);
    cpl_msg_debug(__func__, "angles: %e,%e, scales: %e,%e", xang, yang, xsc, ysc);
    cpl_test_lt(fabs(xang), klimitsINM[type][2]); /* almost no rotation */
    cpl_test_lt(fabs(yang), klimitsINM[type][3]);
    /* should both be about 0.2''/px */
    cpl_test_abs(xsc, 0.2/3600., klimitsINM[type][0]);
    cpl_test_abs(ysc, 0.2/3600., klimitsINM[type][1]);
    /* test the added keywords */
    cpl_test(cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_DETSIGMA) >= 1.0 &&
             cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_DETSIGMA) <= 3.0);
    cpl_test(cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_RADIUS) > 0. &&
             cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_RADIUS) <= 5.);
    cpl_test(cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_ACCURACY) > 0.);
    cpl_test(cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_FACCURACY) > 0. &&
             cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_FACCURACY) <= 5.);
    cpl_test(cpl_table_has_column(wcsobj->detected, "RA"));
    cpl_test(cpl_table_has_column(wcsobj->detected, "DEC"));

    /* reinstate the original detected sources table */
    cpl_table_delete(wcsobj->detected);
    wcsobj->detected = saved;
    unsetenv("MUSE_DEBUG_WCS");
  } /* for type */

  cpl_msg_debug(__func__, "\n____ quadruple based method _____");
  muse_image *image0 = muse_imagelist_get(wcsobj->cube->recimages, 1);
  type = MUSE_WCS_CENTROID_GAUSSIAN; /* Use a single type here */
  cpl_table *detected = muse_wcs_centroid_stars(image0, 3., type);
  /* checks were already done above... */

  /* remove the previous solution */
  cpl_table_delete(wcsobj->detected);
  wcsobj->detected = detected;
  cpl_propertylist_delete(wcsobj->wcs);
  wcsobj->wcs = NULL;
  rc = muse_wcs_solve(wcsobj, wcsref, 5., 0., 2, 3.);
  cpl_msg_debug(__func__, "Solving WCS by quadruples returned rc=%d: %s", rc,
                rc != CPL_ERROR_NONE ? cpl_error_get_message() : "");
  cpl_test(rc == CPL_ERROR_NONE);
  cpl_test_nonnull(wcsobj->wcs);
  /* get output fit properties (using functions tested below...) */
  double xang0, yang0, xsc0, ysc0;
  muse_wcs_get_angles(wcsobj->wcs, &xang0, &yang0);
  muse_wcs_get_scales(wcsobj->wcs, &xsc0, &ysc0);
  cpl_msg_debug(__func__, "angles: %e,%e, scales: %e,%e", xang0, yang0, xsc0, ysc0);
  cpl_test_lt(fabs(xang0), klimitsINM[type][2]); /* almost no rotation */
  cpl_test_lt(fabs(yang0), klimitsINM[type][3]);
  /* should both be about 0.2''/px */
  cpl_test_abs(xsc0, 0.2/3600., klimitsINM[type][0]);
  cpl_test_abs(ysc0, 0.2/3600., klimitsINM[type][1]);
  /* test the added keywords */
  cpl_test(cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_RADIUS) > 0. &&
           cpl_propertylist_get_float(wcsobj->wcs, MUSE_HDR_WCS_RADIUS) <= 5.);
  /* also test copying the WCS solution */
  cpl_propertylist *htest = cpl_propertylist_new();
  cpl_test(muse_wcs_copy_keywords(wcsobj->wcs, htest, 'A', "A test solution")
           == CPL_ERROR_NONE);
  cpl_test(cpl_propertylist_get_size(htest) == 16);
  cpl_test(cpl_propertylist_has(htest, "WCSNAMEA"));
  cpl_test_eq_string("A test solution",
                     cpl_propertylist_get_string(htest, "WCSNAMEA"));
  cpl_test(cpl_propertylist_has(htest, "CD2_2A")); /* two example keywords */
  cpl_test(cpl_propertylist_has(htest, "CUNIT1A"));
  cpl_propertylist_save(htest, "htest.fits", CPL_IO_CREATE);
  /* add an EXTNAME to the header */
  cpl_propertylist_update_string(htest, "EXTNAME", "TEST_EXTENSION");
  cpl_test(muse_wcs_copy_keywords(wcsobj->wcs, htest, 'B', NULL)
           == CPL_ERROR_NONE);
  cpl_test(cpl_propertylist_get_size(htest) == 32);
  cpl_test(cpl_propertylist_has(htest, "CD1_1B")); /* two other example keywords */
  cpl_test(cpl_propertylist_has(htest, "CRVAL2B"));
  cpl_propertylist_save(htest, "htest.fits", CPL_IO_CREATE);
  cpl_errorstate state = cpl_errorstate_get();
  cpl_test(muse_wcs_copy_keywords(NULL, htest, 'Y', "Yippie!!!") == CPL_ERROR_NULL_INPUT);
  cpl_test(!cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_test(muse_wcs_copy_keywords(wcsobj->wcs, NULL, 'Z', NULL) == CPL_ERROR_NULL_INPUT);
  cpl_test(!cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_propertylist_delete(htest);

  /* test some failure cases of muse_wcs_locate_sources() */
  state = cpl_errorstate_get();
  rc = muse_wcs_locate_sources(NULL, 3., MUSE_WCS_CENTROID_BOX, wcsobj);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_locate_sources(ptin, -5., MUSE_WCS_CENTROID_BOX, wcsobj);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_locate_sources(ptin, 0., MUSE_WCS_CENTROID_BOX, wcsobj);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_locate_sources(ptin, 3., -1, wcsobj);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_locate_sources(ptin, 3., MUSE_WCS_CENTROID_BOX + 1, wcsobj);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_locate_sources(ptin, 3., MUSE_WCS_CENTROID_BOX, NULL);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  muse_pixtable *pt2 = cpl_calloc(1, sizeof(muse_pixtable));
  pt2->table = cpl_table_duplicate(ptin->table);
  rc = muse_wcs_locate_sources(pt2, 3., MUSE_WCS_CENTROID_BOX, wcsobj); /* without header */
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  pt2->header = cpl_propertylist_duplicate(ptin->header);
  /* with pixel table without apparent DAR correction headers */
  muse_datacube_delete(wcsobj->cube);
  cpl_table_delete(wcsobj->detected);
  cpl_propertylist_erase_regexp(ptin->header, "ESO DRS MUSE PIXTABLE DAR ", 0);
  rc = muse_wcs_locate_sources(ptin, 3., MUSE_WCS_CENTROID_MOFFAT, wcsobj);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_UNSUPPORTED_MODE);
  cpl_errorstate_set(state);
  /* create pixel table with flat data, should fail object detection */
  cpl_table_fill_column_window_float(pt2->table, MUSE_PIXTABLE_DATA,
                                     0, cpl_table_get_nrow(pt2->table), 100);
  muse_datacube *safe = wcsobj->cube; /* don't want to leak the previous cube */
  wcsobj->cube = NULL;
  rc = muse_wcs_locate_sources(pt2, 3., MUSE_WCS_CENTROID_BOX, wcsobj);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(state);
  muse_datacube_delete(wcsobj->cube); /* delete constant-value cube again */
  wcsobj->cube = safe;
  /* test some failure cases of muse_wcs_centroid_stars() */
  cpl_table *bla = muse_wcs_centroid_stars(NULL, 3., MUSE_WCS_CENTROID_MOFFAT);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(bla);
  muse_image *image = muse_imagelist_get(wcsobj->cube->recimages, 1);
  state = cpl_errorstate_get();
  bla = muse_wcs_centroid_stars(image, 0., MUSE_WCS_CENTROID_MOFFAT);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(bla);
  bla = muse_wcs_centroid_stars(image, 3., MUSE_WCS_CENTROID_BOX + 1);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  /* test some failure cases of muse_wcs_solve() */
  rc = muse_wcs_solve(NULL, wcsref, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_solve(wcsobj, NULL, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_solve(wcsobj, wcsref, 0., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_solve(wcsobj, wcsref, -2., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  /* fake bad entries in the reference table to mess up object identification */
  cpl_table *wcsref2 = cpl_table_duplicate(wcsref);
  cpl_table_fill_column_window(wcsref, "RA", 0, 100000, 10.);
  cpl_table_fill_column_window(wcsref, "DEC", 0, 100000, -10.);
  rc = muse_wcs_solve(wcsobj, wcsref, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(state);
  cpl_table_erase_column(wcsref, "RA");
  rc = muse_wcs_solve(wcsobj, wcsref, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_BAD_FILE_FORMAT);
  cpl_errorstate_set(state);
  cpl_table_delete(wcsref);
  /* don't know how to simulate failure of cpl_wcs_platesol()... */
  /* also test failures of muse_wcs_optimize_solution() */
  state = cpl_errorstate_get();
  rc = muse_wcs_optimize_solution(NULL, -3., MUSE_WCS_CENTROID_GAUSSIAN, wcsref2, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  muse_datacube *cube99 = wcsobj->cube;
  wcsobj->cube = NULL;
  rc = muse_wcs_optimize_solution(wcsobj, -3., MUSE_WCS_CENTROID_GAUSSIAN, wcsref2, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cube99 = wcsobj->cube = cube99;
  char *astname = cpl_strdup(cpl_array_get_string(wcsobj->cube->recnames, 1));
  cpl_array_set_string(wcsobj->cube->recnames, 1, "not_astrometric");
  rc = muse_wcs_optimize_solution(wcsobj, -3., MUSE_WCS_CENTROID_GAUSSIAN, wcsref2, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(state);
  cpl_array_set_string(wcsobj->cube->recnames, 1, astname);
  cpl_free(astname);
  rc = muse_wcs_optimize_solution(wcsobj, 3., MUSE_WCS_CENTROID_GAUSSIAN, wcsref2, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_optimize_solution(wcsobj, -3., MUSE_WCS_CENTROID_BOX + 1, wcsref2, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_optimize_solution(wcsobj, -3., MUSE_WCS_CENTROID_GAUSSIAN, NULL, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_table_name_column(wcsref2, "RA", "2_RA");
  rc = muse_wcs_optimize_solution(wcsobj, -3., MUSE_WCS_CENTROID_GAUSSIAN, wcsref2, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_BAD_FILE_FORMAT);
  cpl_errorstate_set(state);
  cpl_table_name_column(wcsref2, "2_RA", "RA");
  rc = muse_wcs_optimize_solution(wcsobj, -3., MUSE_WCS_CENTROID_GAUSSIAN, wcsref2, -5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_optimize_solution(wcsobj, -3., MUSE_WCS_CENTROID_GAUSSIAN, wcsref2, 5., -5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  rc = muse_wcs_optimize_solution(wcsobj, -0.1, MUSE_WCS_CENTROID_GAUSSIAN, wcsref2, 5., 5., 2, 3.);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_OUTPUT);
  cpl_errorstate_set(state);
  /* don't know how to make the final muse_wcs_solve() call in that function fail... */
  /* clean up this part */
  muse_pixtable_delete(pt2);
  muse_pixtable_delete(ptin);
  cpl_table_delete(wcsref2);
  state = cpl_errorstate_get();
  muse_wcs_object_delete(wcsobj);
  cpl_test(cpl_errorstate_is_equal(state));
  muse_wcs_object_delete(NULL); /* should work with NULL, too */
  cpl_test(cpl_errorstate_is_equal(state));

  /*************************************************
   * create and delete a header with a default WCS *
   *************************************************/
  state = cpl_errorstate_get();
  cpl_propertylist *pwcs = muse_wcs_create_default(NULL);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(!strncmp("RA---TAN", muse_pfits_get_ctype(pwcs, 1), 9) &&
           !strncmp("DEC--TAN", muse_pfits_get_ctype(pwcs, 2), 9));
  state = cpl_errorstate_get();
  muse_wcs *wcs = muse_wcs_new(pwcs);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(wcs->cd11 < 0. && wcs->cd22 > 0. && wcs->cddet != 0.);
  cpl_test_rel(wcs->cd11, -kMuseSpaxelSizeX_WFM/3600., DBL_EPSILON);
  cpl_test_rel(wcs->cd22, kMuseSpaxelSizeY_WFM/3600., DBL_EPSILON);
  cpl_test(wcs->cd12 == 0. && wcs->cd21 == 0.);
  cpl_free(wcs);
  cpl_propertylist_delete(pwcs);

  /* same but giving it a header without interesting entries */
  cpl_propertylist *header = cpl_propertylist_new();
  cpl_propertylist_append_string(header, "ESO SOMETHING", "not WFM");
  state = cpl_errorstate_get();
  pwcs = muse_wcs_create_default(header);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_propertylist_delete(pwcs);

  /* now with with a header with an instrument mode entry */
  cpl_propertylist_append_string(header, "ESO INS MODE", "WFM-AO-N");
  pwcs = muse_wcs_create_default(header);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(!strncmp("RA---TAN", muse_pfits_get_ctype(pwcs, 1), 9) &&
           !strncmp("DEC--TAN", muse_pfits_get_ctype(pwcs, 2), 9));
  state = cpl_errorstate_get();
  wcs = muse_wcs_new(pwcs);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(wcs->cd11 < 0. && wcs->cd22 > 0. && wcs->cddet != 0.);
  cpl_test_rel(wcs->cd11, -kMuseSpaxelSizeX_WFM/3600., DBL_EPSILON);
  cpl_test_rel(wcs->cd22, kMuseSpaxelSizeY_WFM/3600., DBL_EPSILON);
  cpl_test(wcs->cd12 == 0. && wcs->cd21 == 0.);
  cpl_free(wcs);
  cpl_propertylist_delete(pwcs);

  /* finally with with a header with an NFM instrument mode entry */
  cpl_propertylist_update_string(header, "ESO INS MODE", "NFM-AO-N");
  pwcs = muse_wcs_create_default(header);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(!strncmp("RA---TAN", muse_pfits_get_ctype(pwcs, 1), 9) &&
           !strncmp("DEC--TAN", muse_pfits_get_ctype(pwcs, 2), 9));
  state = cpl_errorstate_get();
  wcs = muse_wcs_new(pwcs);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(wcs->cd11 < 0. && wcs->cd22 > 0. && wcs->cddet != 0.);
  cpl_test_rel(wcs->cd11, -kMuseSpaxelSizeX_NFM/3600., DBL_EPSILON);
  cpl_test_rel(wcs->cd22, kMuseSpaxelSizeY_NFM/3600., DBL_EPSILON);
  cpl_test(wcs->cd12 == 0. && wcs->cd21 == 0.);
  cpl_free(wcs);
  cpl_propertylist_delete(pwcs);
  cpl_propertylist_delete(header);

  /****************************************
   * test transformations of pixel tables *
   ****************************************/
  /* use values from WCS Paper II, example 1 as references */
  cpl_propertylist *p = muse_test_wcs_create_example_1_header();
  /* second WCS header, with wrong coordinate type */
  cpl_propertylist *p2 = cpl_propertylist_duplicate(p);
  cpl_propertylist_update_string(p2, "CTYPE1", "BLABLA");
  cpl_propertylist_update_string(p2, "CTYPE2", "BLABLA");
  /* create a pixel table with all the necessary headers/contents needed here */
  muse_pixtable *pt = cpl_calloc(1, sizeof(muse_pixtable));
  pt->table = muse_cpltable_new(muse_pixtable_def, 3);
  pt->header = cpl_propertylist_new();
  /* add some keywords to fake a position angle of zero */
  cpl_propertylist_append_double(pt->header, "ESO TEL PARANG START", 0.);
  cpl_propertylist_append_double(pt->header, "ESO TEL ALT", 75.);
  cpl_propertylist_append_double(pt->header, "ESO INS DROT START", 37.5);
  /* also add the position angle directly */
  cpl_propertylist_append_double(pt->header, "ESO INS DROT POSANG", 0.);
  cpl_propertylist_append_string(pt->header, "ESO INS DROT MODE", "SKY");
  /* set pixel positions from WCS Paper II, example 1 (Table 6) */
  cpl_table_set(pt->table, MUSE_PIXTABLE_XPOS, 0,   1); /* SE corner */
  cpl_table_set(pt->table, MUSE_PIXTABLE_YPOS, 0,   2);
  cpl_table_set(pt->table, MUSE_PIXTABLE_XPOS, 1,   1); /* NE corner */
  cpl_table_set(pt->table, MUSE_PIXTABLE_YPOS, 1, 512);
  cpl_table_set(pt->table, MUSE_PIXTABLE_XPOS, 2, 511); /* NW corner */
  cpl_table_set(pt->table, MUSE_PIXTABLE_YPOS, 2, 512);
  /* override with values, so that CRPIXi in our function *
   * ends up like the one expected by the example         */
  cpl_propertylist_append_float(pt->header, MUSE_HDR_PT_XLO, 0);
  cpl_propertylist_append_float(pt->header, MUSE_HDR_PT_XHI, 0);
  cpl_propertylist_append_float(pt->header, MUSE_HDR_PT_YLO, 0);
  cpl_propertylist_append_float(pt->header, MUSE_HDR_PT_YHI, 0);

  state = cpl_errorstate_get();
  cpl_test(muse_wcs_project_tan(pt, NULL) == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_project_tan(NULL, p) == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  state = cpl_errorstate_get();
  cpl_test(muse_wcs_project_tan(pt, p2) == CPL_ERROR_UNSUPPORTED_MODE);
  cpl_errorstate_set(state);

  cpl_test(muse_wcs_project_tan(pt, p) == CPL_ERROR_NONE);
  /* output units in radians */
  cpl_test(cpl_table_get_column_unit(pt->table, MUSE_PIXTABLE_XPOS) &&
           !strncmp(cpl_table_get_column_unit(pt->table, MUSE_PIXTABLE_XPOS), "rad", 4));
  cpl_test(cpl_table_get_column_unit(pt->table, MUSE_PIXTABLE_YPOS) &&
           !strncmp(cpl_table_get_column_unit(pt->table, MUSE_PIXTABLE_YPOS), "rad", 4));
  /* (xpos,ypos) should now be (phi,theta - pi/2) */
  float *xpos = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_XPOS),
        *ypos = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_YPOS);
  cpl_test_abs(xpos[0], 45.0 / CPL_MATH_DEG_RAD, FLT_EPSILON);
  cpl_test_abs(ypos[0] + CPL_MATH_PI_2, 88.918255 / CPL_MATH_DEG_RAD, FLT_EPSILON);
  cpl_test_abs(xpos[1], 135.0 / CPL_MATH_DEG_RAD, FLT_EPSILON);
  cpl_test_abs(ypos[1] + CPL_MATH_PI_2, 88.918255 / CPL_MATH_DEG_RAD, FLT_EPSILON);
  cpl_test_abs(xpos[2], 225.0 / CPL_MATH_DEG_RAD, FLT_EPSILON);
  cpl_test_abs(ypos[2] + CPL_MATH_PI_2, 88.918255 / CPL_MATH_DEG_RAD, FLT_EPSILON);
  cpl_msg_debug(__func__, "after project_tan: (0: %e,%e, 1: %e,%e, 2: %e,%e) <? %e",
                xpos[0] - 45.0 / CPL_MATH_DEG_RAD,
                ypos[0] + CPL_MATH_PI_2 - 88.918255 / CPL_MATH_DEG_RAD,
                xpos[1] - 135.0 / CPL_MATH_DEG_RAD,
                ypos[1] + CPL_MATH_PI_2 - 88.918255 / CPL_MATH_DEG_RAD,
                xpos[2] - 225.0 / CPL_MATH_DEG_RAD,
                ypos[2] + CPL_MATH_PI_2 - 88.918255 / CPL_MATH_DEG_RAD, FLT_EPSILON);
  /* pixel table now in radians, should fail! */
  state = cpl_errorstate_get();
  cpl_test(muse_wcs_project_tan(pt, p) == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);

  state = cpl_errorstate_get();
  cpl_test(muse_wcs_position_celestial(NULL, 45.83, 63.57) == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_propertylist *pt_header_copy = cpl_propertylist_duplicate(pt->header);
  cpl_propertylist_update_string(pt->header, "CTYPE1", "BLABLA");
  cpl_propertylist_update_string(pt->header, "CTYPE2", "BLABLA");
  state = cpl_errorstate_get();
  cpl_test(muse_wcs_position_celestial(pt, 45.83, 63.57) == CPL_ERROR_UNSUPPORTED_MODE);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(pt->header);
  pt->header = NULL;
  state = cpl_errorstate_get();
  cpl_test(muse_wcs_position_celestial(NULL, 45.83, 63.57) == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  pt->header = pt_header_copy;
  cpl_test(muse_wcs_position_celestial(pt, 45.83, 63.57) == CPL_ERROR_NONE);
  /* output units in degrees */
  cpl_test(cpl_table_get_column_unit(pt->table, MUSE_PIXTABLE_XPOS) &&
           !strncmp(cpl_table_get_column_unit(pt->table, MUSE_PIXTABLE_XPOS), "deg", 4));
  cpl_test(cpl_table_get_column_unit(pt->table, MUSE_PIXTABLE_YPOS) &&
           !strncmp(cpl_table_get_column_unit(pt->table, MUSE_PIXTABLE_YPOS), "deg", 4));
  /* (xpos,ypos) should now be (ra,dec) */
  xpos = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_XPOS),
  ypos = cpl_table_get_data_float(pt->table, MUSE_PIXTABLE_YPOS);
  cpl_test_abs(xpos[0], 47.503264 - 45.83, kLimitDegF);
  cpl_test_abs(ypos[0], 62.795111 - 63.57, kLimitDegF);
  cpl_test_abs(xpos[1], 47.595581 - 45.83, kLimitDegF);
  cpl_test_abs(ypos[1], 64.324332 - 63.57, kLimitDegF);
  cpl_test_abs(xpos[2], 44.064419 - 45.83, kLimitDegF);
  cpl_test_abs(ypos[2], 64.324332 - 63.57, kLimitDegF);
  cpl_msg_debug(__func__, "after position_celestial: (0: %e,%e, 1: %e,%e, 2: %e,%e) <? %e",
                xpos[0] - (47.503264 - 45.83), ypos[0] - (62.795111 - 63.57),
                xpos[1] - (47.595581 - 45.83), ypos[1] - (64.324332 - 63.57),
                xpos[2] - (44.064419 - 45.83), ypos[2] - (64.324332 - 63.57), kLimitDegF);
  /* pixel table now in degrees, should fail! */
  state = cpl_errorstate_get();
  cpl_test(muse_wcs_position_celestial(pt, 45.83, 63.57) == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);

  muse_pixtable_delete(pt);
  cpl_propertylist_delete(p);
  cpl_propertylist_delete(p2);

  /***************************************************
   * test rotation of exposure by zeropoint rotation *
   ***************************************************/
  cpl_propertylist *inexp = cpl_propertylist_new(),
                   *incal = cpl_propertylist_new();
  /* start with no rotation */
  cpl_propertylist_append_string(inexp, "ESO INS DROT MODE", "SKY");
  cpl_propertylist_append_double(inexp, "ESO INS DROT POSANG", 0.);
  cpl_propertylist_append_double(incal, "CD1_1", -0.2/3600.);
  cpl_propertylist_append_double(incal, "CD1_2", 0.);
  cpl_propertylist_append_double(incal, "CD2_1", 0.);
  cpl_propertylist_append_double(incal, "CD2_2", 0.2/3600.);
  cpl_propertylist *cdout = muse_wcs_apply_cd(inexp, incal);
  double xang, yang, xsc, ysc;
  muse_wcs_get_scales(cdout, &xsc, &ysc);
  muse_wcs_get_angles(cdout, &xang, &yang);
  cpl_test_abs(xsc, 0.2/3600., DBL_EPSILON);
  cpl_test_abs(ysc, 0.2/3600., DBL_EPSILON);
  cpl_test_abs(xang, 0., DBL_EPSILON);
  cpl_test_abs(yang, 0., DBL_EPSILON);
  cpl_propertylist_delete(cdout);
  /* standard rotation tested as working fine during Comm1:                       *
   * WCS solution: scales 0.19866269991584470378 / 0.19892623242553914009 arcsec, *
   *   angles 0.57924428478310829860 / -0.27738138025024355882 deg                *
   * Updated CD matrix (-59.99999999999999289457 deg field rotation):             *
   *   -2.736310e-05 4.757814e-05 4.792937e-05 2.811352e-05                       *
   *   (scales 0.19758783554940947957 / 0.20003805495272852788 arcsec,            *
   *   angles -60.09596472295560687371 / -59.60576088395551863641 deg)            */
  cpl_propertylist_append_string(inexp, "DATE-OBS", "2014-02-22T03:57:01.454");
  cpl_propertylist_update_double(inexp, "ESO INS DROT POSANG", 60.);
  cpl_propertylist_update_double(incal, "CD1_1", -5.51812632497E-05);
  cpl_propertylist_update_double(incal, "CD1_2", -5.57886124193E-07);
  cpl_propertylist_update_double(incal, "CD2_1", 2.67511546844E-07);
  cpl_propertylist_update_double(incal, "CD2_2", 5.52566392427E-05);
  cdout = muse_wcs_apply_cd(inexp, incal);
  muse_wcs_get_scales(cdout, &xsc, &ysc);
  muse_wcs_get_angles(cdout, &xang, &yang);
  cpl_test_abs(xsc, 0.19758783554940947957 / 3600., DBL_EPSILON);
  cpl_test_abs(ysc, 0.20003805495272852788 / 3600., DBL_EPSILON);
  cpl_test_abs(xang, -60.09596472295560687371, DBL_EPSILON);
  cpl_test_abs(yang, -59.60576088395551863641, DBL_EPSILON);
  cpl_propertylist_delete(cdout);
  cpl_propertylist_erase(inexp, "DATE-OBS");
  /* 30 deg field rotation, no calibration rotation */
  cpl_propertylist_update_double(inexp, "ESO INS DROT POSANG", -30.);
  cpl_propertylist_update_double(incal, "CD1_1", -0.2/3600.);
  cpl_propertylist_update_double(incal, "CD1_2", 0.);
  cpl_propertylist_update_double(incal, "CD2_1", 0.);
  cpl_propertylist_update_double(incal, "CD2_2", 0.2/3600.);
  cdout = muse_wcs_apply_cd(inexp, incal);
  muse_wcs_get_scales(cdout, &xsc, &ysc);
  muse_wcs_get_angles(cdout, &xang, &yang);
  cpl_test_abs(xsc, 0.2/3600., DBL_EPSILON);
  cpl_test_abs(ysc, 0.2/3600., DBL_EPSILON);
  cpl_test_abs(xang, 30., 20. * DBL_EPSILON);
  cpl_test_abs(yang, 30., 20. * DBL_EPSILON);
  cpl_propertylist_delete(cdout);
  /* 35 deg field rotation, 5. deg calibration rotation */
  cpl_propertylist_update_double(inexp, "ESO INS DROT POSANG", -35.);
  cpl_propertylist_update_double(incal, "CD1_1", 0.2/3600. * cos(5. * CPL_MATH_RAD_DEG));
  cpl_propertylist_update_double(incal, "CD1_2", 0.2/3600. * sin(5. * CPL_MATH_RAD_DEG));
  cpl_propertylist_update_double(incal, "CD2_1", -0.2/3600. * sin(5. * CPL_MATH_RAD_DEG));
  cpl_propertylist_update_double(incal, "CD2_2", 0.2/3600. * cos(5. * CPL_MATH_RAD_DEG));
  cdout = muse_wcs_apply_cd(inexp, incal);
  muse_wcs_get_scales(cdout, &xsc, &ysc);
  muse_wcs_get_angles(cdout, &xang, &yang);
  cpl_test_abs(xsc, 0.2/3600., DBL_EPSILON);
  cpl_test_abs(ysc, 0.2/3600., DBL_EPSILON);
  cpl_test_abs(xang, -40., DBL_EPSILON);
  cpl_test_abs(yang, -40., DBL_EPSILON);
  cpl_propertylist_delete(cdout);
  /* -10. deg field rotation, -3./2. deg calibration rotation */
  cpl_propertylist_update_double(inexp, "ESO INS DROT POSANG", -10.);
  cpl_propertylist_update_double(incal, "CD1_1", -0.2/3600. * cos(-3. * CPL_MATH_RAD_DEG));
  cpl_propertylist_update_double(incal, "CD1_2", -0.2/3600. * sin(-3. * CPL_MATH_RAD_DEG));
  cpl_propertylist_update_double(incal, "CD2_1", 0.2/3600. * sin(-2. * CPL_MATH_RAD_DEG));
  cpl_propertylist_update_double(incal, "CD2_2", 0.2/3600. * cos(-2. * CPL_MATH_RAD_DEG));
  cdout = muse_wcs_apply_cd(inexp, incal);
  muse_wcs_get_scales(cdout, &xsc, &ysc);
  muse_wcs_get_angles(cdout, &xang, &yang);
  cpl_test_abs(xsc, 0.19789511778677520981 / 3600., DBL_EPSILON);
  cpl_test_abs(ysc, 0.20359239586930183430 / 3600., DBL_EPSILON);
  cpl_test_abs(xang, 7.10199686161382981453, 10 * DBL_EPSILON);
  cpl_test_abs(yang, 11.83086044790917945591, DBL_EPSILON);
  cpl_propertylist_delete(cdout);
  /* failures */
  state = cpl_errorstate_get();
  cdout = muse_wcs_apply_cd(NULL, incal);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(cdout);
  cdout = muse_wcs_apply_cd(inexp, NULL);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_test_null(cdout);
  cpl_propertylist_delete(inexp);
  cpl_propertylist_delete(incal);

  /***************************************************************************
   * test transformations between pixels and spherical celestial coordinates *
   ***************************************************************************/
  /* first for a non-rotated WCS with standard MUSE scaling */
  p = muse_test_wcs_create_typical_tan_header();
  cpl_propertylist_append_int(p, "NAXIS", 2); /* needed for cpl_wcs_*() */
  cpl_propertylist_append_int(p, "NAXIS1", 300);
  cpl_propertylist_append_int(p, "NAXIS2", 300);
  muse_test_wcs_check_celestial_transformation(p, "standard MUSE", 1.);

  /* now for a 25 deg rotated WCS with unequal scaling (0.25'' in x, 0.15'' in y) */
  cpl_propertylist_update_double(p, "CD1_1", -6.2938038673665E-5);
  cpl_propertylist_update_double(p, "CD2_2", 3.77628232041998E-5);
  cpl_propertylist_update_double(p, "CD1_2", -2.9348490966691E-5);
  cpl_propertylist_update_double(p, "CD2_1", -1.7609094580015E-5);
  muse_test_wcs_check_celestial_transformation(p, "25 deg rotated", 1.);

  /* same thing again, now closer to the south pole */
  cpl_propertylist_update_double(p, "CRVAL1", 300.);
  cpl_propertylist_update_double(p, "CRVAL2", -87.5);
  cpl_propertylist_update_double(p, "RA", 300.);
  cpl_propertylist_update_double(p, "DEC", -87.5);
  /* more generous comparison in this case */
  muse_test_wcs_check_celestial_transformation(p, "near pole", 11.2);

  /* different region of the sky */
  cpl_propertylist_update_double(p, "CRVAL1", 111.);
  cpl_propertylist_update_double(p, "CRVAL2", +25.5);
  cpl_propertylist_update_double(p, "RA", 111.);
  cpl_propertylist_update_double(p, "DEC", +25.5);
  /* more generous comparison in this case */
  muse_test_wcs_check_celestial_transformation(p, "northern", 1.);

  /* check failure cases */
  double v1, v2;
  state = cpl_errorstate_get();
  cpl_test(muse_wcs_celestial_from_pixel(NULL, 1., 1., &v1, &v2)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_celestial_from_pixel(p, 1., 1., NULL, &v2)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_celestial_from_pixel(p, 1., 1., &v1, NULL)
           == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_test(muse_wcs_pixel_from_celestial(NULL, 10., 10., &v1, &v2)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_pixel_from_celestial(p, 10., 10., NULL, &v2)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_pixel_from_celestial(p, 10., 10., &v1, NULL)
           == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_propertylist_update_string(p, "CTYPE1", "RA_not_TAN");
  cpl_propertylist_update_string(p, "CTYPE2", "DEC_not_TAN");
  cpl_test(muse_wcs_celestial_from_pixel(p, 1., 1., &v1, &v2)
           == CPL_ERROR_UNSUPPORTED_MODE);
  cpl_test(muse_wcs_pixel_from_celestial(p, 10., 10., &v1, &v2)
           == CPL_ERROR_UNSUPPORTED_MODE);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(p);

  /***************************************************************************
   * test transformations between projection plane and celestial coordinates *
   ***************************************************************************/
  /* use values from WCS Paper II, example 1 as references */
  p = muse_test_wcs_create_example_1_header();
  double x, y;
  cpl_test(muse_wcs_projplane_from_celestial(p, 47.503264, 62.795111, &x, &y)
           == CPL_ERROR_NONE);
  cpl_test(fabs(x - 0.765000) < kLimitPPl && fabs(y - (-0.765000)) < kLimitPPl);
  cpl_msg_debug(__func__, "SE corner: %f,%f (%e,%e <? %e", x, y, x - 0.765000,
                y - (-0.765000), kLimitPPl);
  cpl_test(muse_wcs_projplane_from_celestial(p, 47.595581, 64.324332, &x, &y)
           == CPL_ERROR_NONE);
  cpl_test(fabs(x - 0.765000) < kLimitPPl && fabs(y - 0.765000) < kLimitPPl);
  cpl_msg_debug(__func__, "NE corner: %f,%f (%e,%e <? %e", x, y, x - 0.765000,
                y - 0.765000, kLimitPPl);
  cpl_test(muse_wcs_projplane_from_celestial(p, 44.064419, 64.324332, &x, &y)
           == CPL_ERROR_NONE);
  cpl_test(fabs(x - (-0.765000)) < kLimitPPl && fabs(y - 0.765000) < kLimitPPl);
  cpl_msg_debug(__func__, "NW corner: %f,%f (%e,%e <? %e", x, y, x - (-0.765000),
                y - 0.765000, kLimitPPl);
  state = cpl_errorstate_get();
  cpl_test(muse_wcs_projplane_from_celestial(NULL, 1., 1., &v1, &v2)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_projplane_from_celestial(p, 1., 1., NULL, &v2)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_projplane_from_celestial(p, 1., 1., &v1, NULL)
           == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_propertylist_update_string(p, "CTYPE1", "RA_not_TAN");
  cpl_propertylist_update_string(p, "CTYPE2", "DEC_not_TAN");
  cpl_test(muse_wcs_projplane_from_celestial(p, 1., 1., &v1, &v2)
           == CPL_ERROR_UNSUPPORTED_MODE);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(p);

  /************************************************************************
   * test transformations between pixels and projection plane coordinates *
   ************************************************************************/
  /* test a non-rotated WCS with standard MUSE scaling */
  p = muse_test_wcs_create_typical_lin_header();
  int i = -1;
  while (kPx[++i][0] > 0) {
    double xpx, ypx,
           xexpect = (kPx[i][0] - 1.) * 1. - 150.,
           yexpect = (kPx[i][1] - 1.) * 1. - 150.;

    /* to projection plane */
    cpl_test(muse_wcs_projplane_from_pixel(p, kPx[i][0], kPx[i][1], &x, &y)
             == CPL_ERROR_NONE);
    cpl_test(fabs(x - xexpect) < kLimitPix);
    cpl_test(fabs(y - yexpect) < kLimitPix);

    /* back to pixel plane */
    cpl_test(muse_wcs_pixel_from_projplane(p, x, y, &xpx, &ypx)
             == CPL_ERROR_NONE);
    cpl_test(fabs(xpx - kPx[i][0]) < kLimitPix);
    cpl_test(fabs(ypx - kPx[i][1]) < kLimitPix);
    cpl_msg_debug(__func__, "%s: x,y = %f,%f (%d,%d, %e,%e <? %e)", "standard MUSE",
                  x, y, kPx[i][0], kPx[i][1], x - kPx[i][0], y - kPx[i][1],
                  kLimitPix);
  } /* while */
  state = cpl_errorstate_get();
  cpl_test(muse_wcs_projplane_from_pixel(NULL, 1., 1., &v1, &v2)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_projplane_from_pixel(p, 1., 1., NULL, &v2)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_projplane_from_pixel(p, 1., 1., &v1, NULL)
           == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_test(muse_wcs_pixel_from_projplane(NULL, 150., 150., &v1, &v2)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_pixel_from_projplane(p, 150., 150., NULL, &v2)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_pixel_from_projplane(p, 150., 150., &v1, NULL)
           == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(p);

  /*************************
   * test muse_wcs getters *
   *************************/
  /* first for a non-rotated WCS with standard MUSE scaling */
  p = muse_test_wcs_create_typical_tan_header();
  cpl_test(muse_wcs_get_angles(p, &xang, &yang) == CPL_ERROR_NONE);
  cpl_test(xang == 0. && yang == 0.);
  cpl_test(muse_wcs_get_scales(p, &xsc, &ysc) == CPL_ERROR_NONE);
  cpl_test(xsc == 0.2 / 3600. && ysc == 0.2 / 3600.);

  /* now for a 25 deg rotated WCS with unequal scaling (0.25'' in x, 0.15'' in y) */
  cpl_propertylist_update_double(p, "CD1_1", -6.2938038673665E-5);
  cpl_propertylist_update_double(p, "CD2_2", 3.77628232041998E-5);
  cpl_propertylist_update_double(p, "CD1_2", -2.9348490966691E-5);
  cpl_propertylist_update_double(p, "CD2_1", -1.7609094580015E-5);
  cpl_test(muse_wcs_get_angles(p, &xang, &yang) == CPL_ERROR_NONE);
  cpl_test(fabs(xang - 25.) < kLimitRot && fabs(yang - 25.) < kLimitRot);
  cpl_test(muse_wcs_get_scales(p, &xsc, &ysc) == CPL_ERROR_NONE);
  cpl_test(fabs(xsc - 0.25 / 3600.) < kLimitSca &&
           fabs(ysc - 0.15 / 3600.) < kLimitSca);
  cpl_msg_debug(__func__, "angles: %e,%e (%e,%e <? %e), scales: %e,%e (%e,%e <? %e)",
                xang, yang, xang - 25., yang - 25., kLimitRot,
                xsc, ysc, xsc - 0.25 / 3600., ysc - 0.15 / 3600., kLimitSca);

  state = cpl_errorstate_get();
  cpl_test(muse_wcs_get_angles(NULL, &xang, &yang) == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_get_scales(NULL, &xsc, &ysc) == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_get_angles(p, NULL, &yang) == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_get_scales(p, NULL, &ysc) == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_get_angles(p, &xang, NULL) == CPL_ERROR_NULL_INPUT);
  cpl_test(muse_wcs_get_scales(p, &xsc, NULL) == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_propertylist_erase_regexp(p, "CD1_?", 0);
  cpl_test(muse_wcs_get_angles(p, &xang, &yang) == CPL_ERROR_DATA_NOT_FOUND);
  cpl_test(muse_wcs_get_scales(p, &xsc, &ysc) == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(state);
  cpl_propertylist_delete(p);

  /**************************
   * test muse_wcs creation *
   **************************/
  /* create and delete a wcs without a header */
  state = cpl_errorstate_get();
  wcs = muse_wcs_new(NULL);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(wcs->cd11 == 1. && wcs->cd22 == 1. && wcs->cddet == 1.);
  cpl_test(wcs->cd12 == 0. && wcs->cd21 == 0. && wcs->crpix1 == 0.
           && wcs->crpix2 == 0. && wcs->crval1 == 0. && wcs->crval2 == 0.);
  cpl_test(wcs->iscelsph == CPL_FALSE);
  cpl_free(wcs);

  /* create and delete a wcs without a header */
  p = muse_test_wcs_create_typical_tan_header();
  state = cpl_errorstate_get();
  wcs = muse_wcs_new(p);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(wcs->cd11 == -0.2 / 3600. && wcs->cd22 == 0.2 / 3600. &&
           wcs->cddet == -pow(0.2 / 3600., 2));
  cpl_test(wcs->cd12 == 0. && wcs->cd21 == 0.);
  cpl_test(wcs->crval1 == 10. && wcs->crpix1 == 156.);
  cpl_test(wcs->crval2 == -30. && wcs->crpix2 == 151.);
  cpl_test(wcs->iscelsph == CPL_FALSE);
  cpl_free(wcs);
  /* should work with debugging on, too */
  cpl_test(setenv("MUSE_DEBUG_WCS", "1", 1) == 0);
  state = cpl_errorstate_get();
  wcs = muse_wcs_new(p);
  cpl_free(wcs);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test(unsetenv("MUSE_DEBUG_WCS") == 0);
  cpl_propertylist_delete(p);

  /* create and delete a wcs with a bad header (singular CD matrix) */
  p = muse_test_wcs_create_typical_tan_header();
  cpl_propertylist_update_double(p, "CD1_1", 0.);
  cpl_propertylist_update_double(p, "CD2_2", 0.);
  state = cpl_errorstate_get();
  wcs = muse_wcs_new(p);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_SINGULAR_MATRIX);
  cpl_errorstate_set(state);
  cpl_test_nonnull(wcs);
  cpl_free(wcs);

  /* use the same bad header to test other functions   *
   * that depend on the invertability of the CD matrix */
  state = cpl_errorstate_get();
  x = y = 0.;
  cpl_test(muse_wcs_pixel_from_projplane(p, 10., -10., &x, &y)
           == CPL_ERROR_SINGULAR_MATRIX);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_SINGULAR_MATRIX);
  cpl_errorstate_set(state);
  cpl_test(isnan(x) && isnan(y));
  x = y = 0.;
  cpl_test(muse_wcs_pixel_from_celestial(p, 10., -10, &x, &y)
           == CPL_ERROR_SINGULAR_MATRIX);
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_SINGULAR_MATRIX);
  cpl_errorstate_set(state);
  cpl_test(isnan(x) && isnan(y));
  cpl_propertylist_delete(p);

  return cpl_test_end(0);
}
