/* -*- 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>

/*----------------------------------------------------------------------------*/
/**
  @brief    test program to check that the functions from the muse_geo module
            do what they should when called with the necessary data
 */
/*----------------------------------------------------------------------------*/
int main(int argc, char **argv)
{
  UNUSED_ARGUMENTS(argc, argv);
  cpl_test_init(PACKAGE_BUGREPORT, CPL_MSG_DEBUG);

  cpl_table *geo = cpl_table_load(BASEFILENAME"_regular.fits", 1, 1);
  cpl_test_nonnull(geo);
  if (!geo) {
    cpl_msg_error(__func__, "could not load \"%s_regular.fits\"", BASEFILENAME);
    return cpl_test_end(1);
  }
  setenv("MUSE_GEOMETRY_PINHOLE_DY", "0.646150", 1);

  /* test muse_geo_table_extract_ifu() */
  cpl_errorstate state = cpl_errorstate_get();
  cpl_table *table = muse_geo_table_extract_ifu(geo, 5);
  cpl_test_nonnull(table);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_table_delete(table);
  state = cpl_errorstate_get();
  table = muse_geo_table_extract_ifu(NULL, 5); /* NULL input table */
  cpl_test_null(table);
  cpl_test(cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  table = muse_geo_table_extract_ifu(geo, 25); /* wrong IFU */
  cpl_test_null(table);
  cpl_test(cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  /* test with incomplete IFU */
  cpl_table *geobad = cpl_table_duplicate(geo);
  cpl_table_select_all(geobad);
  cpl_table_and_selected_int(geobad, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, 2);
  cpl_table_and_selected_int(geobad, MUSE_GEOTABLE_CCD, CPL_EQUAL_TO, 2);
  cpl_table_erase_selected(geobad);
  table = muse_geo_table_extract_ifu(geobad, 2); /* missing slice in IFU */
  cpl_test_null(table);
  cpl_test(cpl_error_get_code() == CPL_ERROR_ILLEGAL_OUTPUT);
  cpl_table_delete(geobad);
  cpl_errorstate_set(state);

  /* test muse_geo_table_ifu_area() */
  double scale = 60. / 1.705 / 10.,
         area5  = muse_geo_table_ifu_area(geo, 5, scale),
         area23 = muse_geo_table_ifu_area(geo, 23, scale);
  cpl_msg_debug(__func__, "areas: %f %f", area5, area23);
  cpl_test_eq(area5, area23); /* regular table, should give same results */
  /* reference area is ~3858.55 pix**2 or ~0.553049 cm**2 in this weird regular table */
  double refarea = 12. * 75./300. * scale
                 * 2 * (1.0709096735174 + 1.07272685657848) / 288. * scale;
  cpl_test_abs(area5, refarea, FLT_EPSILON);
  cpl_test(cpl_errorstate_is_equal(state));
  /* failures */
  cpl_test_zero(muse_geo_table_ifu_area(NULL, 5, scale));
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  geobad = cpl_table_extract(geo, 0, 10); /* too few slices */
  cpl_test_zero(muse_geo_table_ifu_area(geobad, 1, scale));
  cpl_test(!cpl_errorstate_is_equal(state) &&
           cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_table_delete(geobad);

  cpl_table_delete(geo);

  /* muse_geo_lines_get() */
  cpl_table *tlines = cpl_table_load(BASEFILENAME"_linelist.fits", 1, 1);
  state = cpl_errorstate_get();
  cpl_vector *vlines = muse_geo_lines_get(tlines);
  cpl_test_nonnull(vlines);
  cpl_test(cpl_errorstate_is_equal(state));
  /* check that some important lines are in there */
  cpl_test_abs(cpl_vector_get(vlines, 0), 5085.822, 1e-3);
  cpl_test_abs(cpl_vector_get(vlines, cpl_vector_find(vlines, 5460.)), 5460.75, 1e-3);
  cpl_test_abs(cpl_vector_get(vlines, cpl_vector_find(vlines, 6678.)), 6678.276, 1e-3);
  cpl_test_abs(cpl_vector_get(vlines, cpl_vector_get_size(vlines) - 1), 8377.507, 1e-3);
  cpl_vector_delete(vlines);
  /* now the failure cases */
  state = cpl_errorstate_get();
  vlines = muse_geo_lines_get(NULL);
  cpl_test_null(vlines);
  cpl_test(cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  /* remove all NeI lines to trigger the too-few-lines error */
  cpl_table_unselect_all(tlines);
  cpl_table_or_selected_string(tlines, MUSE_LINE_CATALOG_ION, CPL_EQUAL_TO, "NeI");
  cpl_table_erase_selected(tlines);
  state = cpl_errorstate_get();
  vlines = muse_geo_lines_get(tlines);
  cpl_test_null(vlines);
  cpl_test(cpl_error_get_code() == CPL_ERROR_DATA_NOT_FOUND);
  cpl_errorstate_set(state);
  cpl_table_delete(tlines);

  /* muse_geo_measure_spots() */
  cpl_table *tr22 = cpl_table_load(BASEFILENAME"_trace22.fits", 1, 1),
            *tr23 = cpl_table_load(BASEFILENAME"_trace23.fits", 1, 1),
            *wv22 = cpl_table_load(BASEFILENAME"_wave22.fits", 1, 1),
            *wv23 = cpl_table_load(BASEFILENAME"_wave23.fits", 1, 1);
  if (!tr22 || !tr23 || !wv22 || !wv23) {
    cpl_error_set_message(__func__, cpl_error_get_code(),
                          "failed to load file(s): %s%s%s%s",
                          !tr22 ? "\""BASEFILENAME"_trace22.fits\" " : "",
                          !tr23 ? "\""BASEFILENAME"_trace23.fits\" " : "",
                          !wv22 ? "\""BASEFILENAME"_wave22.fits\" " : "",
                          !wv23 ? "\""BASEFILENAME"_wave23.fits\" " : "");
    cpl_table_delete(tr22);
    cpl_table_delete(tr23);
    cpl_table_delete(wv22);
    cpl_table_delete(wv23);
    return cpl_test_end(1);
  }
  muse_image *image = muse_image_new();
  image->data = cpl_image_new(5, 5, CPL_TYPE_FLOAT);
  image->dq = cpl_image_new(5, 5, CPL_TYPE_INT);
  image->stat = cpl_image_new(5, 5, CPL_TYPE_FLOAT);
  image->header = cpl_propertylist_new();
  muse_imagelist *list = muse_imagelist_new(),
                 *list4 = muse_imagelist_new();
  muse_imagelist_set(list, muse_image_duplicate(image), 0);
  muse_imagelist_set(list, muse_image_duplicate(image), 1);
  muse_imagelist_set(list, muse_image_duplicate(image), 2);
  muse_imagelist_set(list, muse_image_duplicate(image), 3);
  muse_imagelist_set(list, muse_image_duplicate(image), 4);
  muse_imagelist_set(list4, muse_image_duplicate(image), 0);
  muse_imagelist_set(list4, muse_image_duplicate(image), 1);
  muse_imagelist_set(list4, muse_image_duplicate(image), 2);
  muse_imagelist_set(list4, muse_image_duplicate(image), 3);
  cpl_vector *lines = cpl_vector_new(5);
  cpl_vector_set(lines, 0, 6143.0625);
  cpl_vector_set(lines, 1, 6506.52783203125);
  cpl_vector_set(lines, 2, 6678.2763671875);
  cpl_vector_set(lines, 3, 6929.46728515625);
  cpl_vector_set(lines, 4, 7245.16650390625);
  state = cpl_errorstate_get();
  cpl_table *spots = muse_geo_measure_spots(NULL, list, tr22, wv22, lines, 3.,
                                            MUSE_GEO_CENTROID_BARYCENTER);
  cpl_test_null(spots);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  spots = muse_geo_measure_spots(image, NULL, tr22, wv22, lines, 3.,
                                 MUSE_GEO_CENTROID_BARYCENTER);
  cpl_test_null(spots);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  spots = muse_geo_measure_spots(image, list, NULL, wv22, lines, 3.,
                                 MUSE_GEO_CENTROID_BARYCENTER);
  cpl_test_null(spots);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  spots = muse_geo_measure_spots(image, list, tr22, NULL, lines, 3.,
                                 MUSE_GEO_CENTROID_GAUSSIAN);
  cpl_test_null(spots);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  spots = muse_geo_measure_spots(image, list, tr22, wv22, NULL, 3.,
                                 MUSE_GEO_CENTROID_BARYCENTER);
  cpl_test_null(spots);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  /* supposed to hit the non-positive sigma error: */
  spots = muse_geo_measure_spots(image, list, tr22, wv22, lines, 0.,
                                 MUSE_GEO_CENTROID_GAUSSIAN);
  cpl_test_null(spots);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_vector *lines2 = cpl_vector_duplicate(lines);
  cpl_vector_set_size(lines2, 2);
  /* supposed to hit too-few-images-in-list error: */
  spots = muse_geo_measure_spots(image, list4, tr22, wv22, lines, 3.,
                                 MUSE_GEO_CENTROID_GAUSSIAN);
  cpl_test_null(spots);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  /* supposed to hit too-few-lines error: */
  spots = muse_geo_measure_spots(image, list, tr22, wv22, lines2, 3.,
                                 MUSE_GEO_CENTROID_BARYCENTER);
  cpl_test_null(spots);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_vector_delete(lines2);
  /* supposed to hit bad-centroiding-type error: */
  spots = muse_geo_measure_spots(image, list, tr22, wv22, lines, 3.,
                                 MUSE_GEO_CENTROID_GAUSSIAN + 1);
  cpl_test_null(spots);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  /* XXX the next one would be the working test, but I  *
   *     need real test images for before activating it */
  //spots = muse_geo_measure_spots(image, list, tr22, wv22, lines, 3.,
  //                               MUSE_GEO_CENTROID_BARYCENTER);
  //cpl_test_nonnull(spots);
  // XXX other tests
  //cpl_table_delete(spots);
  muse_image_delete(image);
  muse_imagelist_delete(list);
  muse_imagelist_delete(list4);
  cpl_vector_delete(lines);

  cpl_table *sp22 = cpl_table_load(BASEFILENAME"_spots22.fits", 1, 1),
            *sp23 = cpl_table_load(BASEFILENAME"_spots23.fits", 1, 1);
  if (!sp22 || !sp23) {
    cpl_error_set_message(__func__, cpl_error_get_code(),
                          "failed to load file(s): %s%s",
                          !sp22 ? "\""BASEFILENAME"_spots22.fits\" " : "",
                          !sp23 ? "\""BASEFILENAME"_spots23.fits\" " : "");
    cpl_table_delete(tr22);
    cpl_table_delete(tr23);
    cpl_table_delete(wv22);
    cpl_table_delete(wv23);
    cpl_table_delete(sp22);
    cpl_table_delete(sp23);
    return cpl_test_end(1);
  }
  /* add the Scale column that is now necessary */
  cpl_table_new_column(sp22, "ScaleFOV", CPL_TYPE_DOUBLE);
  cpl_table_new_column(sp23, "ScaleFOV", CPL_TYPE_DOUBLE);
  cpl_table_fill_column_window(sp22, "ScaleFOV", 0, cpl_table_get_nrow(sp22), 60./35.);
  cpl_table_fill_column_window(sp23, "ScaleFOV", 0, cpl_table_get_nrow(sp23), 60./35.);
  /* remove any line measurement outside the range *
   * 6143..7246to simplify the tests below         */
  cpl_table_unselect_all(sp22);
  cpl_table_or_selected_double(sp22, "lambda", CPL_LESS_THAN, 6143.);
  cpl_table_or_selected_double(sp22, "lambda", CPL_GREATER_THAN, 7245.);
  cpl_table_erase_selected(sp22);
  cpl_table_unselect_all(sp23);
  cpl_table_or_selected_double(sp23, "lambda", CPL_LESS_THAN, 6143.);
  cpl_table_or_selected_double(sp23, "lambda", CPL_GREATER_THAN, 7245.);
  cpl_table_erase_selected(sp23);

  /* muse_geo_compute_pinhole_local_distance() */
  /* start testing possible failures */
  cpl_array *ady = cpl_array_new(0, CPL_TYPE_DOUBLE);
  state = cpl_errorstate_get();
  cpl_test(muse_geo_compute_pinhole_local_distance(NULL, sp22)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(!cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_test(muse_geo_compute_pinhole_local_distance(ady, NULL)
           == CPL_ERROR_NULL_INPUT);
  cpl_test(!cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_table *spbad = cpl_table_duplicate(sp22);
  cpl_table_set_size(spbad, 9);
  cpl_test(muse_geo_compute_pinhole_local_distance(ady, spbad)
           == CPL_ERROR_ILLEGAL_INPUT);
  cpl_test(!cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_table_delete(spbad);
  spbad = cpl_table_duplicate(sp23);
  cpl_table_erase_column(spbad, "flux");
  cpl_test(muse_geo_compute_pinhole_local_distance(ady, spbad)
           == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_test(!cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_table_delete(spbad);
  spbad = cpl_table_duplicate(sp23);
  cpl_table_set_int(spbad, "SubField", 1, 22);
  cpl_test(muse_geo_compute_pinhole_local_distance(ady, spbad)
           == CPL_ERROR_ILLEGAL_INPUT);
  cpl_test(!cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_table_delete(spbad);
  spbad = cpl_table_duplicate(sp23);
  cpl_table_set_double(spbad, "ScaleFOV", 3, 1.715);
  cpl_test(muse_geo_compute_pinhole_local_distance(ady, spbad)
           == CPL_ERROR_ILLEGAL_INPUT);
  cpl_test(!cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_table_delete(spbad);
  cpl_array *dybad = cpl_array_new(10, CPL_TYPE_INT);
  cpl_test(muse_geo_compute_pinhole_local_distance(dybad, spbad)
           == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_test(!cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_array_delete(dybad);
  /* success */
  state = cpl_errorstate_get();
  cpl_test(muse_geo_compute_pinhole_local_distance(ady, sp22) == CPL_ERROR_NONE);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test_eq(cpl_array_get_size(ady), 2558);
  cpl_msg_debug("ady", "%f .. %f (results from sp22)", cpl_array_get_min(ady),
                cpl_array_get_max(ady));
  cpl_test(cpl_array_get_min(ady) > 0.20 && cpl_array_get_max(ady) < 0.80);
  /* add results from a 2nd IFU */
  cpl_test(muse_geo_compute_pinhole_local_distance(ady, sp23) == CPL_ERROR_NONE);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test_eq(cpl_array_get_size(ady), 5239);
  cpl_msg_debug("ady", "%f .. %f (results from sp22 and sp23)",
                cpl_array_get_min(ady), cpl_array_get_max(ady));
  cpl_test(cpl_array_get_min(ady) > 0.20 && cpl_array_get_max(ady) < 0.80);
#if 0
  FILE *fp = fopen("ady.dat", "w");
  cpl_array_dump(ady, 0, cpl_array_get_size(ady), fp);
  fclose(fp);
#endif

  /* muse_geo_compute_pinhole_global_distance() */
  /* start testing possible failures */
  state = cpl_errorstate_get();
  cpl_test(muse_geo_compute_pinhole_global_distance(NULL, 0.1, 0.5, 0.8) == 0.0);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code()
           == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_array *abad = cpl_array_new(10, CPL_TYPE_DOUBLE_COMPLEX); /* bad array type */
  cpl_test(muse_geo_compute_pinhole_global_distance(abad, 0.1, 0.5, 0.8) == 0.0);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code()
           == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_array_delete(abad);
  abad = cpl_array_new(10, CPL_TYPE_DOUBLE) /* only invalid entries */;
  cpl_test(muse_geo_compute_pinhole_global_distance(abad, 0.1, 0.5, 0.8) == 0.0);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code()
           == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_array_delete(abad);
  /* test real functionality */
  state = cpl_errorstate_get();
  double refvalue = atof(getenv("MUSE_GEOMETRY_PINHOLE_DY")),
         value = muse_geo_compute_pinhole_global_distance(ady, 0.001, 0.5, 0.8);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test_rel(value, refvalue, 0.0005);
  cpl_array_delete(ady);

  /* muse_geo_determine_initial() */
  /* start testing possible failures */
  state = cpl_errorstate_get();
  muse_geo_table *geoi = muse_geo_determine_initial(NULL, tr22);
  cpl_test_null(geoi);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  geoi = muse_geo_determine_initial(sp22, NULL);
  cpl_test_null(geoi);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  spbad = cpl_table_duplicate(sp22);
  cpl_table_set_size(spbad, 9);
  geoi = muse_geo_determine_initial(spbad, tr22); /* too few spots */
  cpl_test_null(geoi);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_table_delete(spbad);
  spbad = cpl_table_duplicate(sp22);
  cpl_table_erase_column(spbad, "VPOS");
  geoi = muse_geo_determine_initial(spbad, tr22); /* missing column */
  cpl_test_null(geoi);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_table_delete(spbad);
  spbad = cpl_table_duplicate(sp22);
  cpl_table_set_int(spbad, "SubField", 5, 5);
  geoi = muse_geo_determine_initial(spbad, tr22); /* multiple IFU numbers */
  cpl_test_null(geoi);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_table_fill_column_window_int(spbad, "SubField", 0, cpl_table_get_nrow(spbad), 25);
  geoi = muse_geo_determine_initial(spbad, tr22); /* bad IFU number */
  cpl_test_null(geoi);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_table_delete(spbad);
  spbad = cpl_table_duplicate(sp22);
  cpl_table_set_double(spbad, "ScaleFOV", 4, 1.713);
  geoi = muse_geo_determine_initial(spbad, tr22); /* bad ScaleFOV in one row */
  cpl_test_null(geoi);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_table_delete(spbad);
  cpl_table *trbad = cpl_table_duplicate(tr22);
  cpl_table_set_size(trbad, 10); /* delete many slices */
  geoi = muse_geo_determine_initial(sp22, trbad); /* bad trace */
  cpl_test(!cpl_errorstate_is_equal(state) &&
           strstr(cpl_error_get_function(), "muse_trace_table_get_polys_for_slice"));
  cpl_errorstate_set(state);
  cpl_test_nonnull(geoi); /* should have produced valid output */
  cpl_table_select_all(geoi->table);
  cpl_table_and_selected_int(geoi->table, MUSE_GEOTABLE_CCD, CPL_EQUAL_TO, 11);
  cpl_test_zero(cpl_table_count_selected(geoi->table)); /* but not for slice 11 any more */
  cpl_table_or_selected_int(geoi->table, MUSE_GEOTABLE_CCD, CPL_EQUAL_TO, 10);
  cpl_test(cpl_table_count_selected(geoi->table) > 0); /* only up to slice 10 */
  muse_geo_table_delete(geoi);
  cpl_table_delete(trbad);
  /* test real functionality */
  state = cpl_errorstate_get();
  muse_geo_table *geoi22 = muse_geo_determine_initial(sp22, tr22),
                 *geoi23 = muse_geo_determine_initial(sp23, tr23);
  cpl_test_nonnull(geoi22);
  cpl_test_nonnull(geoi23);
  cpl_test(cpl_errorstate_is_equal(state));
#if 0
  cpl_table_save(geoi22->table, NULL, NULL, "geoi22.fits", CPL_IO_CREATE);
  cpl_table_save(geoi23->table, NULL, NULL, "geoi23.fits", CPL_IO_CREATE);
#endif
  cpl_msg_debug(__func__, "rows in tables geoi22/geoi23: %d/%d, "
                "%f .. %f (+/- %f) / %f .. %f (+/- %f)",
                (int)cpl_table_get_nrow(geoi22->table),
                (int)cpl_table_get_nrow(geoi23->table),
                cpl_table_get_column_min(geoi22->table, "lambda"),
                cpl_table_get_column_max(geoi22->table, "lambda"),
                cpl_table_get_column_stdev(geoi22->table, "lambda"),
                cpl_table_get_column_min(geoi23->table, "lambda"),
                cpl_table_get_column_max(geoi23->table, "lambda"),
                cpl_table_get_column_stdev(geoi23->table, "lambda"));
  cpl_test(cpl_table_get_nrow(geoi22->table) >= 50);
  cpl_test(cpl_table_get_nrow(geoi23->table) >= 50);
  /* some data should be present for each wavelength, *
   * equal distribution would give stdev = 417.715    */
  cpl_test(cpl_table_get_column_min(geoi22->table, "lambda") > 6143. &&
           cpl_table_get_column_max(geoi22->table, "lambda") < 7246. &&
           fabs(cpl_table_get_column_stdev(geoi22->table, "lambda") - 300.) < 20.);
  cpl_test(cpl_table_get_column_min(geoi23->table, "lambda") > 6143. &&
           cpl_table_get_column_max(geoi23->table, "lambda") < 7246. &&
           fabs(cpl_table_get_column_stdev(geoi23->table, "lambda") - 300.) < 20.);
  double amin22 = cpl_table_get_column_min(geoi22->table, MUSE_GEOTABLE_ANGLE),
         amax22 = cpl_table_get_column_max(geoi22->table, MUSE_GEOTABLE_ANGLE),
         amin23 = cpl_table_get_column_min(geoi23->table, MUSE_GEOTABLE_ANGLE),
         amax23 = cpl_table_get_column_max(geoi23->table, MUSE_GEOTABLE_ANGLE),
         wmin22 = cpl_table_get_column_min(geoi22->table, MUSE_GEOTABLE_WIDTH),
         wmax22 = cpl_table_get_column_max(geoi22->table, MUSE_GEOTABLE_WIDTH),
         wmin23 = cpl_table_get_column_min(geoi23->table, MUSE_GEOTABLE_WIDTH),
         wmax23 = cpl_table_get_column_max(geoi23->table, MUSE_GEOTABLE_WIDTH);
  cpl_msg_debug(__func__, "table extrema after initial computation: "
                "angles %f...%f/%f...%f, widths %f...%f, %f...%f",
                amin22, amax22, amin23, amax23, wmin22, wmax22, wmin23, wmax23);
  cpl_test(amin22 > -0.50 && amax22 < 0.50);
  cpl_test(amin23 > -0.50 && amax23 < 0.10);
  cpl_test(wmin22 > 75. && wmax22 < 80.);
  cpl_test(wmin23 > 75. && wmax23 < 80.);

  /* muse_geo_determine_horizontal() */
  /* start testing possible failures */
  state = cpl_errorstate_get();
  muse_geo_table *geoh = muse_geo_determine_horizontal(NULL);
  cpl_test_null(geoh);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table *gibad = muse_geo_table_duplicate(geoi22);
  cpl_table_set_size(gibad->table, 49);
  geoh = muse_geo_determine_horizontal(gibad); /* too few entries */
  cpl_test_null(geoh);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table_delete(gibad);
  gibad = muse_geo_table_duplicate(geoi22);
  cpl_table_erase_column(gibad->table, MUSE_GEOTABLE_Y);
  geoh = muse_geo_determine_horizontal(gibad); /* missing column */
  cpl_test_null(geoh);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table_delete(gibad);
  gibad = muse_geo_table_duplicate(geoi22);
  cpl_table_fill_column_window_int(gibad->table, MUSE_GEOTABLE_FIELD,
                                   0, cpl_table_get_nrow(gibad->table), 25);
  geoh = muse_geo_determine_horizontal(gibad); /* bad IFU number */
  cpl_test_null(geoh);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_table_fill_column_window_int(gibad->table, MUSE_GEOTABLE_FIELD,
                                   0, cpl_table_get_nrow(gibad->table), 22);
  cpl_table_fill_column_window_int(gibad->table, MUSE_GEOTABLE_FIELD, 0, 10, 12);
  geoh = muse_geo_determine_horizontal(gibad); /* multiple IFU numbers */
  cpl_test_null(geoh);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table_delete(gibad);
  /* test real functionality */
  state = cpl_errorstate_get();
  muse_geo_table *geoh22 = muse_geo_determine_horizontal(geoi22),
                 *geoh23 = muse_geo_determine_horizontal(geoi23);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test_nonnull(geoh22);
  cpl_test_nonnull(geoh23);
#if 0
  cpl_table_save(geoh22->table, NULL, NULL, "geoh22.fits", CPL_IO_CREATE);
  cpl_table_save(geoh23->table, NULL, NULL, "geoh23.fits", CPL_IO_CREATE);
#endif
  cpl_test(cpl_table_get_nrow(geoh22->table) == 48);
  cpl_test(cpl_table_get_nrow(geoh23->table) == 48);
  muse_geo_table *geohboth[2] = { geoh22, geoh23 };
  unsigned char itable;
  for (itable = 0; itable <= 1; itable++) {
    int irow;
    for (irow = 0; irow < cpl_table_get_nrow(geohboth[itable]->table); irow++) {
      int err;
      unsigned short slice = cpl_table_get_int(geohboth[itable]->table,
                                               MUSE_GEOTABLE_CCD, irow, &err);
      cpl_test(!err);
      double x = cpl_table_get_double(geohboth[itable]->table, MUSE_GEOTABLE_X, irow,
                                      &err);
      cpl_test(!err);
      /* XXX these ranges are still pretty wide... */
      double dref = 116.6, /* slice > 36 */
             dlimit = 0.4;
      if (slice <= 12) {
        dref = -116.21;
        dlimit = 0.42;
      } else if (slice <= 24) {
        dref = -38.8;
        dlimit = 0.35;
      } else if (slice <= 36) {
        dref = 38.8;
        dlimit = 0.3;
      }
      if (fabs(x - dref) >= dlimit) { /* debug output if test is going to fail */
        cpl_msg_debug(__func__, "table %d slice %02hu irow %d: x = %f",
                      itable == 0 ? 22 : 23, slice, irow, x);
      } /* if */
      cpl_test(fabs(x - dref) < dlimit);
    } /* for irow */
  } /* for itable (both tables) */
  amin22 = cpl_table_get_column_min(geoh22->table, MUSE_GEOTABLE_ANGLE),
  amax22 = cpl_table_get_column_max(geoh22->table, MUSE_GEOTABLE_ANGLE),
  amin23 = cpl_table_get_column_min(geoh23->table, MUSE_GEOTABLE_ANGLE),
  amax23 = cpl_table_get_column_max(geoh23->table, MUSE_GEOTABLE_ANGLE),
  wmin22 = cpl_table_get_column_min(geoh22->table, MUSE_GEOTABLE_WIDTH),
  wmax22 = cpl_table_get_column_max(geoh22->table, MUSE_GEOTABLE_WIDTH),
  wmin23 = cpl_table_get_column_min(geoh23->table, MUSE_GEOTABLE_WIDTH),
  wmax23 = cpl_table_get_column_max(geoh23->table, MUSE_GEOTABLE_WIDTH);
  cpl_msg_debug(__func__, "table extrema after horizontal computation: "
                "angles %f...%f/%f...%f, widths %f...%f, %f...%f",
                amin22, amax22, amin23, amax23, wmin22, wmax22, wmin23, wmax23);
  cpl_test(amin22 > -0.50 && amax22 < 0.25); /* still asymmetric but not the same as above */
  cpl_test(amin23 > -0.50 && amax23 < 0.10);
  cpl_test(wmin22 > 76. && wmax22 < 79.);
  cpl_test(wmin23 > 76. && wmax23 < 79.);
  /* check that muse_geo_determine_horizontal() also works with the original gap computation */
  cpl_test(setenv("MUSE_GEOMETRY_STD_GAP", "1", 1) == 0);
  state = cpl_errorstate_get();
  muse_geo_table *geoh22x = muse_geo_determine_horizontal(geoi22),
                 *geoh23x = muse_geo_determine_horizontal(geoi23);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test_nonnull(geoh22x);
  cpl_test_nonnull(geoh23x);
  cpl_test(cpl_table_get_nrow(geoh22x->table) == 48);
  cpl_test(cpl_table_get_nrow(geoh23x->table) == 48);
  muse_geo_table_delete(geoi22);
  muse_geo_table_delete(geoi23);
  muse_geo_table_delete(geoh22x);
  muse_geo_table_delete(geoh23x);

  /* merge tables */
  geoh = geoh22;
  state = cpl_errorstate_get();
  cpl_table_insert(geoh->table, geoh23->table, cpl_table_get_nrow(geoh->table));
  muse_geo_table_delete(geoh23);
  cpl_test(cpl_errorstate_is_equal(state));

  /* muse_geo_refine_horizontal() */
  /* load another test dataset to test this function */
  cpl_table *geocomb = cpl_table_load(BASEFILENAME"_geo_comb01to03.fits", 1, 1),
            *spotscomb = cpl_table_load(BASEFILENAME"_spots01to03.fits", 1, 1);
  /* add new necessary ScaleFOV columns */
  cpl_table_new_column(spotscomb, "ScaleFOV", CPL_TYPE_DOUBLE);
  cpl_table_fill_column_window(spotscomb, "ScaleFOV", 0, cpl_table_get_nrow(spotscomb), 60./35.);
  muse_geo_table *gtcomb = cpl_calloc(1, sizeof(muse_geo_table));
  gtcomb->table = geocomb;
  gtcomb->scale = 60./35.;
  /* start testing possible failures */
  state = cpl_errorstate_get();
  cpl_test(muse_geo_refine_horizontal(NULL, spotscomb) == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  cpl_test(muse_geo_refine_horizontal(gtcomb, NULL) == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table *geocbad = muse_geo_table_duplicate(gtcomb);
  cpl_table *spots2 = cpl_table_duplicate(spotscomb);
  cpl_table_set_size(geocbad->table, 17);
  cpl_test(muse_geo_refine_horizontal(geocbad, spots2) == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table_delete(geocbad);
  geocbad = muse_geo_table_duplicate(gtcomb);
  cpl_table_erase_column(geocbad->table, "spot");
  cpl_test(muse_geo_refine_horizontal(geocbad, spots2) == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table_delete(geocbad);
  geocbad = muse_geo_table_duplicate(gtcomb);
  cpl_table_erase_column(spots2, "dxcen");
  cpl_test(muse_geo_refine_horizontal(geocbad, spots2) == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  cpl_table_delete(spots2);
  spots2 = cpl_table_duplicate(spotscomb);
  cpl_table_fill_column_window(geocbad->table, MUSE_GEOTABLE_FIELD,
                               0, cpl_table_get_nrow(geocbad->table), 5);
  cpl_test(muse_geo_refine_horizontal(geocbad, spots2) == CPL_ERROR_DATA_NOT_FOUND);
  muse_geo_table_delete(geocbad);
  cpl_table_delete(spots2);
  spots2 = cpl_table_duplicate(spotscomb);
  cpl_table_set_double(spots2, "ScaleFOV", 10, 1.713);
  cpl_test(muse_geo_refine_horizontal(gtcomb, spots2) == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  cpl_table_delete(spots2);
  /* now test successes */
  cpl_errorstate_set(state);
  /* check positions of slice 18 in the first three IFUs before the shift */
  double ifu03pos18 = cpl_table_get_double(gtcomb->table, MUSE_GEOTABLE_X, 17, NULL),
         ifu01pos18 = cpl_table_get_double(gtcomb->table, MUSE_GEOTABLE_X, 65, NULL),
         ifu02pos18 = cpl_table_get_double(gtcomb->table, MUSE_GEOTABLE_X, 113, NULL),
         d01to02 = ifu01pos18 - ifu02pos18,
         d02to03 = ifu02pos18 - ifu03pos18;
  cpl_msg_debug(__func__, "input differences: %f / %f", d01to02, d02to03);
  /* save the table for another test later */
  muse_geo_table *gtcomb2 = muse_geo_table_duplicate(gtcomb);
  cpl_table *spotscomb2 = cpl_table_duplicate(spotscomb);
  cpl_test(muse_geo_refine_horizontal(gtcomb, spotscomb) == CPL_ERROR_NONE);
  cpl_test(cpl_errorstate_is_equal(state));
  double ifu03pos18new = cpl_table_get_double(gtcomb->table, MUSE_GEOTABLE_X, 17, NULL),
         ifu01pos18new = cpl_table_get_double(gtcomb->table, MUSE_GEOTABLE_X, 65, NULL),
         ifu02pos18new = cpl_table_get_double(gtcomb->table, MUSE_GEOTABLE_X, 113, NULL),
         d01to02new = ifu01pos18new - ifu02pos18new,
         d02to03new = ifu02pos18new - ifu03pos18new;
  cpl_msg_debug(__func__, "output differences: %f / %f", d01to02new, d02to03new);
  /* the result should lie somewhere between the values that were  *
   * printed for each slicer stack (those I checked independently) */
  cpl_test_abs(d01to02new - d01to02, (-0.198 + 0.542) / 2., (0.542 + 0.198) / 2.);
  cpl_test_abs(d02to03new - d02to03, (1.725 + 2.083) / 2., (2.083 - 1.725) / 2.);
  muse_geo_table_delete(gtcomb);
  cpl_table_delete(spotscomb);
  /* try again, with the additional override for six IFUs in place */
  cpl_test(setenv("MUSE_GEOMETRY_HORI_OFFSETS", "2.0,0.,1.5,0.,0.,0.", 1) == 0);
  state = cpl_errorstate_get();
  cpl_test(muse_geo_refine_horizontal(gtcomb2, spotscomb2) == CPL_ERROR_NONE);
  cpl_test(cpl_errorstate_is_equal(state));
  ifu03pos18new = cpl_table_get_double(gtcomb2->table, MUSE_GEOTABLE_X, 17, NULL);
  ifu01pos18new = cpl_table_get_double(gtcomb2->table, MUSE_GEOTABLE_X, 65, NULL);
  ifu02pos18new = cpl_table_get_double(gtcomb2->table, MUSE_GEOTABLE_X, 113, NULL);
  d01to02new = ifu01pos18new - ifu02pos18new;
  d02to03new = ifu02pos18new - ifu03pos18new;
  cpl_msg_debug(__func__, "output differences (with HORI_OFFSETS): %f / %f",
                d01to02new, d02to03new);
  /* the result should have the additional shifts now */
  cpl_test_abs(d01to02new - d01to02, (-0.198 + 0.542) / 2.+ 2., (0.542 + 0.198) / 2.);
  cpl_test_abs(d02to03new - d02to03, (1.725 + 2.083) / 2., (2.083 - 1.725) / 2.);
  unsetenv("MUSE_GEOMETRY_HORI_OFFSETS");
  muse_geo_table_delete(gtcomb2);
  cpl_table_delete(spotscomb2);

  /* muse_geo_determine_vertical() */
  /* start testing possible failures */
  state = cpl_errorstate_get();
  muse_geo_table *geov = muse_geo_determine_vertical(NULL);
  cpl_test_null(geov);
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table *geohbad = muse_geo_table_duplicate(geoh);
  cpl_table_set_size(geohbad->table, 9);
  geov = muse_geo_determine_vertical(geohbad); /* too few rows */
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table_delete(geohbad);
  geohbad = muse_geo_table_duplicate(geoh);
  cpl_table_erase_column(geohbad->table, "vpos");
  geov = muse_geo_determine_vertical(geohbad); /* column "vpos" is missing */
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_INCOMPATIBLE_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table_delete(geohbad);
  geohbad = muse_geo_table_duplicate(geoh);
  cpl_table_set_int(geohbad->table, "spot", 10, 1);
  cpl_table_set_int(geohbad->table, "spot", 12, 3);
  geov = muse_geo_determine_vertical(geohbad); /* other spots still in the table */
  cpl_test(!cpl_errorstate_is_equal(state) && cpl_error_get_code() == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table_delete(geohbad);
  /* test real functionality */
  state = cpl_errorstate_get();
  geov = muse_geo_determine_vertical(geoh);
  cpl_test(cpl_errorstate_is_equal(state));
  cpl_test_nonnull(geov);
  /* needs to contain mandatory columns and error estimates */
  cpl_test(cpl_table_has_column(geov->table, MUSE_GEOTABLE_FIELD) &&
           cpl_table_has_column(geov->table, MUSE_GEOTABLE_CCD) &&
           cpl_table_has_column(geov->table, MUSE_GEOTABLE_SKY) &&
           cpl_table_has_column(geov->table, MUSE_GEOTABLE_X) &&
           cpl_table_has_column(geov->table, MUSE_GEOTABLE_Y) &&
           cpl_table_has_column(geov->table, MUSE_GEOTABLE_ANGLE) &&
           cpl_table_has_column(geov->table, MUSE_GEOTABLE_WIDTH) &&
           cpl_table_has_column(geov->table, MUSE_GEOTABLE_X"err") &&
           cpl_table_has_column(geov->table, MUSE_GEOTABLE_Y"err") &&
           cpl_table_has_column(geov->table, MUSE_GEOTABLE_ANGLE"err") &&
           cpl_table_has_column(geov->table, MUSE_GEOTABLE_WIDTH"err"));
  cpl_test(cpl_table_get_column_min(geov->table, MUSE_GEOTABLE_FIELD) == 22 &&
           cpl_table_get_column_max(geov->table, MUSE_GEOTABLE_FIELD) == 23);
  /* check distances in the slicer stacks */
  cpl_propertylist *sorting = cpl_propertylist_new();
  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_Y, CPL_TRUE);
  unsigned char nstack;
  for (nstack = 1; nstack <= 4; nstack++) {
    unsigned short nslice1 = 37, nslice2 = 48; /* for stack 1 */
    if (nstack == 2) {
      nslice1 = 25, nslice2 = 36;
    } else if (nstack == 3) {
      nslice1 = 13, nslice2 = 24;
    } else if (nstack == 4) {
      nslice1 = 1, nslice2 = 12;
    }
    cpl_table_select_all(geov->table);
    cpl_table_and_selected_int(geov->table, MUSE_GEOTABLE_SKY, CPL_NOT_LESS_THAN,
                               nslice1);
    cpl_table_and_selected_int(geov->table, MUSE_GEOTABLE_SKY, CPL_NOT_GREATER_THAN,
                               nslice2);
    cpl_table *geov2 = cpl_table_extract_selected(geov->table);
    cpl_table_sort(geov2, sorting); /* sort by vertical distance */
#if 0
    printf("table for slicer stack %d:\n", nstack);
    cpl_table_dump(geov2, 0, 1000, stdout);
    fflush(stdout);
#endif
    int err;
    double y = cpl_table_get_double(geov2, MUSE_GEOTABLE_Y, 0, &err);
    cpl_test(!err);
    unsigned short sky1 = cpl_table_get_int(geov2, MUSE_GEOTABLE_SKY, 0, &err);
    cpl_test(!err);
    int irow;
    for (irow = 1; irow < cpl_table_get_nrow(geov2); irow++) {
      double y2 = cpl_table_get_double(geov2, MUSE_GEOTABLE_Y, irow, &err),
             dy = y - y2;
      cpl_test(!err);
      unsigned short sky2 = cpl_table_get_int(geov2, MUSE_GEOTABLE_SKY, irow, &err);
      cpl_test(!err);
      cpl_msg_debug("dy", "row %d of %d, sky slices %d and %d --> %f",
                    irow, (int)cpl_table_get_nrow(geov2), sky1, sky2, dy);
      cpl_test((sky2 - sky1) == 1 || (sky2 - sky1) == -11);
      //cpl_test(dy > 1.01 && dy < 1.30); /* [pix] XXX this should be around 1.0?! */
      /* allow a big range, since the top/bottom slices  *
       * will have only a small distance to its neighbor */
      cpl_test(dy > 0.65 && dy < 1.26);
      y = y2; /* keep these values... */
      sky1 = sky2; /* ... for the next loop iteration */
    } /* for irow */
    cpl_table_delete(geov2);
  } /* for nstack */
  cpl_propertylist_delete(sorting);
  muse_geo_table_delete(geoh);

  /* muse_geo_finalize() */
  /* start testing possible failures */
  state = cpl_errorstate_get();
  cpl_error_code rc = muse_geo_finalize(NULL);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_NULL_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table *geovbad = muse_geo_table_duplicate(geov);
  cpl_table_erase_column(geovbad->table, MUSE_GEOTABLE_X);
  rc = muse_geo_finalize(geovbad);
  cpl_test(!cpl_errorstate_is_equal(state) && rc == CPL_ERROR_ILLEGAL_INPUT);
  cpl_errorstate_set(state);
  muse_geo_table_delete(geovbad);
  /* test real functionality */
  muse_geo_table *geovkeep = muse_geo_table_duplicate(geov);
  state = cpl_errorstate_get();
  /* first pretend that no pinhole vertical distance was set */
  cpl_test(!unsetenv("MUSE_GEOMETRY_PINHOLE_DY"));
  rc = muse_geo_finalize(geov);
  cpl_test(cpl_errorstate_is_equal(state) && rc == CPL_ERROR_NONE);
#if 0
  cpl_table_dump(geov, 0, 10000, stdout);
  fflush(stdout);
#endif
  cpl_table_select_all(geov->table);
  cpl_table_and_selected_double(geov->table, MUSE_GEOTABLE_WIDTH, CPL_EQUAL_TO, 0.);
  cpl_test(cpl_table_count_selected(geov->table) == 0);
  cpl_table_and_selected_double(geov->table, MUSE_GEOTABLE_ANGLE, CPL_EQUAL_TO, 0.);
  cpl_test(cpl_table_count_selected(geov->table) == 0);
  cpl_size nrow2 = cpl_table_get_nrow(geov->table);
  cpl_test(nrow2 == 2 * kMuseSlicesPerCCD); /* 2 IFUs */
  /* the rest of the table should be identical once sorted           *
   * when compared to the one before the call to muse_geo_finalize() */
  cpl_table_erase_selected(geov->table); /* remove the new filler rows */
  cpl_test(cpl_table_get_nrow(geovkeep->table) == cpl_table_get_nrow(geov->table));
  sorting = cpl_propertylist_new();
  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_FIELD, CPL_FALSE);
  cpl_propertylist_append_bool(sorting, MUSE_GEOTABLE_SKY, CPL_FALSE);
  cpl_table_sort(geovkeep->table, sorting);
  cpl_propertylist_delete(sorting);
  int irow;
  for (irow = 0; irow < cpl_table_get_nrow(geovkeep->table); irow++) {
    cpl_test(cpl_table_get_int(geovkeep->table, MUSE_GEOTABLE_FIELD, irow, NULL)
             == cpl_table_get_int(geov->table, MUSE_GEOTABLE_FIELD, irow, NULL));
    cpl_test(cpl_table_get_int(geovkeep->table, MUSE_GEOTABLE_CCD, irow, NULL)
             == cpl_table_get_int(geov->table, MUSE_GEOTABLE_CCD, irow, NULL));
    cpl_test(cpl_table_get_int(geovkeep->table, MUSE_GEOTABLE_SKY, irow, NULL)
             == cpl_table_get_int(geov->table, MUSE_GEOTABLE_SKY, irow, NULL));
    cpl_test(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_X, irow, NULL)
             == cpl_table_get_double(geov->table, MUSE_GEOTABLE_X, irow, NULL));
    cpl_test(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_WIDTH, irow, NULL)
             == cpl_table_get_double(geov->table, MUSE_GEOTABLE_WIDTH, irow, NULL));
    /* by default, the y position and the angle should now be reversed */
    cpl_test(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_Y, irow, NULL)
             == -cpl_table_get_double(geov->table, MUSE_GEOTABLE_Y, irow, NULL));
    cpl_test(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_ANGLE, irow, NULL)
             == -cpl_table_get_double(geov->table, MUSE_GEOTABLE_ANGLE, irow, NULL));
  } /* for irow */
  /* test with some missing table rows,                   *
   * should get reinstated with angle=width=0 and x=y=NAN */
  double y3 = cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_Y, 2, NULL),
         y9 = cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_Y, 8, NULL),
         angle3 = cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_ANGLE, 2, NULL),
         angle9 = cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_ANGLE, 8, NULL),
         x3 = cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_X, 2, NULL),
         x9 = cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_X, 8, NULL),
         width3 = cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_WIDTH, 2, NULL),
         width9 = cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_WIDTH, 8, NULL);
  /* reset the pinhole distance again with which this table was created */
  cpl_test(!setenv("MUSE_GEOMETRY_PINHOLE_DY", "0.664870", 1));
  cpl_size nrow = cpl_table_get_nrow(geovkeep->table);
  cpl_table_erase_window(geovkeep->table, 0, 2);
  state = cpl_errorstate_get();
  rc = muse_geo_finalize(geovkeep);
  cpl_test(cpl_errorstate_is_equal(state) && rc == CPL_ERROR_NONE);
  cpl_test_eq(nrow, cpl_table_get_nrow(geovkeep->table));
  cpl_test(isnan(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_X, 0, NULL)));
  cpl_test(isnan(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_Y, 0, NULL)));
  cpl_test(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_ANGLE, 0, NULL) == 0.);
  cpl_test(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_WIDTH, 0, NULL) == 0.);
  /* the y and angle columns should now be adapted to what they had before, *
   * but with the sign reversed (everything gets flipped by default)        */
  double fdy = -kMuseCUmpmDY / 0.664870;
  cpl_test_abs(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_Y, 2, NULL), y3 * fdy,
               DBL_EPSILON);
  cpl_test_abs(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_Y, 8, NULL), y9 * fdy,
               DBL_EPSILON);
  /* tan() is almost linear, use this for the check */
  cpl_test_abs(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_ANGLE, 2, NULL),
               angle3 * fdy, 4. * FLT_EPSILON);
  cpl_test_abs(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_ANGLE, 8, NULL),
               angle9 * fdy, 11. * FLT_EPSILON);
  /* x and width should be unchanged */
  cpl_test_eq(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_X, 2, NULL), x3);
  cpl_test_eq(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_X, 8, NULL), x9);
  cpl_test_eq(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_WIDTH, 2, NULL), width3);
  cpl_test_eq(cpl_table_get_double(geovkeep->table, MUSE_GEOTABLE_WIDTH, 8, NULL), width9);
  /* try again without inversion */
  muse_geo_table *geovkeep2 = muse_geo_table_duplicate(geovkeep);
  cpl_test(setenv("MUSE_GEOMETRY_NO_INVERSION", "1", 1) == 0);
  state = cpl_errorstate_get();
  rc = muse_geo_finalize(geovkeep2);
  cpl_test(cpl_errorstate_is_equal(state) && rc == CPL_ERROR_NONE);
  cpl_test_eq(cpl_table_get_nrow(geovkeep2->table), cpl_table_get_nrow(geovkeep->table));
  cpl_test(unsetenv("MUSE_GEOMETRY_NO_INVERSION") == 0);
  muse_geo_table_delete(geovkeep2);
  muse_geo_table_delete(geovkeep);

#if 0
  state = cpl_errorstate_get();
  const char *fn = "muse_test_geo_final.fits";
  cpl_msg_info(__func__, "Saving final table as \"%s\"", fn);
  cpl_table_save(geov->table, NULL, NULL, fn, CPL_IO_CREATE);
  cpl_test(cpl_errorstate_is_equal(state));
#endif
  cpl_table_delete(tr22);
  cpl_table_delete(tr23);
  cpl_table_delete(wv22);
  cpl_table_delete(wv23);
  cpl_table_delete(sp22);
  cpl_table_delete(sp23);

  /* muse_geo_correct_slices() */
  state = cpl_errorstate_get();
  geovkeep = muse_geo_table_duplicate(geov);
  rc = muse_geo_correct_slices(geov, NULL, 1.5);
  cpl_test(rc == CPL_ERROR_NONE && cpl_errorstate_is_equal(state));
  muse_geo_table_delete(geov);
  /* again with header */
  geov = muse_geo_table_duplicate(geovkeep);
  cpl_propertylist *header = cpl_propertylist_new();
  rc = muse_geo_correct_slices(geov, header, 1.5);
  cpl_test(rc == CPL_ERROR_NONE && cpl_errorstate_is_equal(state));
  cpl_test(cpl_propertylist_get_size(header) == 4);
#if 0
  state = cpl_errorstate_get();
  const char *fn = "muse_test_geo_smoothed.fits";
  cpl_msg_info(__func__, "Saving final table as \"%s\"", fn);
  cpl_table_save(geov->table, header, NULL, fn, CPL_IO_CREATE);
  cpl_test(cpl_errorstate_is_equal(state));
#endif
  cpl_propertylist_delete(header);
  muse_geo_table_delete(geov);
  /* test failures */
  state = cpl_errorstate_get();
  rc = muse_geo_correct_slices(NULL, NULL, 1.5);
  cpl_test(rc == CPL_ERROR_NULL_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  rc = muse_geo_correct_slices(geovkeep, NULL, 0.); /* non-positive sigma */
  cpl_test(rc == CPL_ERROR_ILLEGAL_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  geov = muse_geo_table_duplicate(geovkeep);
  cpl_table_delete(geov->table);
  geov->table = NULL; /* simulate missing table component */
  rc = muse_geo_correct_slices(geov, NULL, 1.5);
  cpl_test(rc == CPL_ERROR_NULL_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  muse_geo_table_delete(geov);
  /* another duplicate for muse_geo_qc_global() testing below */
  muse_geo_table *geov3 = muse_geo_table_duplicate(geovkeep);
  /* test wrong columns */
  const char *cols[] = { MUSE_GEOTABLE_FIELD, MUSE_GEOTABLE_CCD, MUSE_GEOTABLE_SKY,
                         MUSE_GEOTABLE_X, MUSE_GEOTABLE_X"err",
                         MUSE_GEOTABLE_Y, MUSE_GEOTABLE_Y"err",
                         MUSE_GEOTABLE_ANGLE, MUSE_GEOTABLE_ANGLE"err",
                         MUSE_GEOTABLE_WIDTH, MUSE_GEOTABLE_WIDTH"err", NULL
                         },
             **col;
  for (col = cols; *col; col++) {
    const char *colname = *col;
    cpl_msg_debug(__func__, "column name %s", colname);

    geov = muse_geo_table_duplicate(geovkeep);
    /* the column type decides which tests to run */
    cpl_type coltype = cpl_table_get_column_type(geov->table, colname);
    cpl_table_erase_column(geov->table, colname);
    state = cpl_errorstate_get();
    rc = muse_geo_correct_slices(geov, NULL, 1.5);
    cpl_test(rc == CPL_ERROR_DATA_NOT_FOUND && !cpl_errorstate_is_equal(state));
    cpl_errorstate_set(state);
    if (coltype != CPL_TYPE_DOUBLE) {
      muse_geo_table_delete(geov);
      continue;
    }
    /* for columns that are double originally, test int and float as well */
    cpl_table_new_column(geov->table, colname, CPL_TYPE_INT);
    state = cpl_errorstate_get();
    rc = muse_geo_correct_slices(geov, NULL, 1.5);
    cpl_test(rc == CPL_ERROR_INCOMPATIBLE_INPUT&& !cpl_errorstate_is_equal(state));
    cpl_errorstate_set(state);
    cpl_table_cast_column(geov->table, colname, colname, CPL_TYPE_FLOAT);
    rc = muse_geo_correct_slices(geov, NULL, 1.5);
    cpl_test(rc == CPL_ERROR_INCOMPATIBLE_INPUT&& !cpl_errorstate_is_equal(state));
    cpl_errorstate_set(state);
    muse_geo_table_delete(geov);
  } /* for cols (relevant table columns */
  muse_geo_table_delete(geovkeep);

  /* test muse_geo_qc_global() */
  header = cpl_propertylist_new();
#if 0
  cpl_table_save(geov3->table, header, NULL, "muse_test_geov3.fits",
                 CPL_IO_CREATE);
#endif
  state = cpl_errorstate_get();
  rc = muse_geo_qc_global(geov3, header);
  cpl_test(rc == CPL_ERROR_NONE && cpl_errorstate_is_equal(state));
  /* should have created 4 entries, 2 gaps and  */
#if 0
  cpl_propertylist_save(header, "bla1.fits", CPL_IO_CREATE);
  system("dfits bla1.fits");
  remove("bla1.fits");
#endif
  cpl_test_eq(cpl_propertylist_get_size(header), 5);
  cpl_test_abs(cpl_propertylist_get_double(header, QC_GEO_GAPS_MEAN),
               0., FLT_EPSILON); /* close to zero for this test dataset */
  /* failures */
  state = cpl_errorstate_get();
  rc = muse_geo_qc_global(NULL, header);
  cpl_test(rc == CPL_ERROR_NULL_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  cpl_propertylist_delete(header);
  state = cpl_errorstate_get();
  rc = muse_geo_qc_global(geov3, NULL);
  cpl_test(rc == CPL_ERROR_NULL_INPUT && !cpl_errorstate_is_equal(state));
  cpl_errorstate_set(state);
  /* partial failure: one missing central slice (IFU 22 SliceSky 17) */
  header = cpl_propertylist_new();
  state = cpl_errorstate_get();
  cpl_table_unselect_all(geov3->table);
  cpl_table_or_selected_int(geov3->table, MUSE_GEOTABLE_FIELD, CPL_EQUAL_TO, 22);
  cpl_table_and_selected_int(geov3->table, MUSE_GEOTABLE_SKY, CPL_EQUAL_TO, 17);
  cpl_table_erase_selected(geov3->table);
  rc = muse_geo_qc_global(geov3, header);
  cpl_test(rc == CPL_ERROR_NONE && cpl_errorstate_is_equal(state));
#if 0
  cpl_propertylist_save(header, "bla2.fits", CPL_IO_CREATE);
  system("dfits bla2.fits");
  remove("bla2.fits");
#endif
  /* should have created one entry less (IFU22 GAP MEAN missing, too) */
  cpl_test_eq(cpl_propertylist_get_size(header), 4);
  cpl_test_abs(cpl_propertylist_get_double(header, QC_GEO_GAPS_MEAN),
               0., FLT_EPSILON); /* close to zero for this test dataset */
  cpl_propertylist_delete(header);
  muse_geo_table_delete(geov3);

  return cpl_test_end(0);
}
