/*
 *  $Id: line.c 29755 2026-04-09 10:31:00Z yeti-dn $
 *  Copyright (C) 2025-2026 David Nečas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  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.
 */

#include "tests/testlibgwy.h"

enum {
    SOME_NUMBER = 43161,
};

static void
assert_line_content(GwyLine *line, const gdouble *ref_data, guint ref_n, gdouble eps)
{
    g_assert_true(GWY_IS_LINE(line));
    guint n = gwy_line_get_res(line);
    g_assert_cmpuint(n, ==, ref_n);
    const gdouble *data = gwy_line_get_data_const(line);
    g_assert_nonnull(data);

    if (eps > 0.0) {
        for (guint i = 0; i < ref_n; i++)
            g_assert_cmpfloat_with_epsilon(data[i], ref_data[i], eps);
    }
    else {
        for (guint i = 0; i < ref_n; i++)
            g_assert_cmpfloat(data[i], ==, ref_data[i]);
    }
}

void
line_assert_equal(GObject *object, GObject *reference)
{
    g_assert_true(GWY_IS_LINE(object));
    g_assert_true(GWY_IS_LINE(reference));

    GwyLine *line = GWY_LINE(object), *line_ref = GWY_LINE(reference);
    g_assert_cmpint(gwy_line_get_res(line), ==, gwy_line_get_res(line_ref));
    g_assert_cmpfloat(gwy_line_get_real(line), ==, gwy_line_get_real(line_ref));
    g_assert_cmpfloat(gwy_line_get_offset(line), ==, gwy_line_get_offset(line_ref));
    g_assert_true(gwy_unit_equal(gwy_line_get_unit_x(line), gwy_line_get_unit_x(line_ref)));
    g_assert_true(gwy_unit_equal(gwy_line_get_unit_y(line), gwy_line_get_unit_y(line_ref)));
    assert_line_content(line, gwy_line_get_data(line_ref), gwy_line_get_res(line_ref), 0.0);
}

void
test_line_basic(void)
{
    const gdouble zeros[5] = { 0, 0, 0, 0, 0 };
    const gdouble twos[5] = { 2, 2, 2, 2, 2 };

    GwyLine *line = gwy_line_new(5, 1.6, TRUE);
    assert_line_content(line, zeros, 5, 0.0);
    g_assert_cmpfloat(gwy_line_get_real(line), ==, 1.6);
    g_assert_cmpfloat(gwy_line_get_dx(line), ==, 1.6/5);
    g_assert_cmpfloat(gwy_line_get_offset(line), ==, 0);

    gwy_line_set_real(line, 100);
    gwy_line_set_offset(line, -14);
    g_assert_cmpfloat(gwy_line_get_real(line), ==, 100);
    g_assert_cmpfloat(gwy_line_get_dx(line), ==, 20);
    g_assert_cmpfloat(gwy_line_get_offset(line), ==, -14);

    gwy_line_fill(line, 2.0);
    assert_line_content(line, twos, 5, 0.0);

    gwy_line_clear(line);
    assert_line_content(line, zeros, 5, 0.0);

    g_assert_cmpfloat(gwy_line_get_real(line), ==, 100);
    g_assert_cmpfloat(gwy_line_get_dx(line), ==, 20);
    g_assert_cmpfloat(gwy_line_get_offset(line), ==, -14);

    g_assert_finalize_object(line);
}

void
test_line_data_changed(void)
{
    GwyLine *line = gwy_line_new(1, 1.0, TRUE);
    guint item_changed = 0;
    g_signal_connect_swapped(line, "data-changed", G_CALLBACK(record_signal), &item_changed);
    gwy_line_data_changed(line);
    g_assert_cmpuint(item_changed, ==, 1);
    gwy_line_data_changed(line);
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_finalize_object(line);
}

static GwyLine*
create_line_for_serialisation(void)
{
    GwyLine *line = gwy_line_new(13, 26.0, FALSE);
    gwy_line_set_offset(line, G_LN2);
    gdouble *data = gwy_line_get_data(line);
    for (guint i = 0; i < 13; i++)
        data[i] = sqrt(i) - G_PI;

    return line;
}

static void
set_line_units_for_serialisation(GwyLine *line)
{
    gwy_unit_set_from_string(gwy_line_get_unit_x(line), "m");
    gwy_unit_set_from_string(gwy_line_get_unit_y(line), "V");
}

void
test_line_serialization(void)
{
    GwyLine *line = create_line_for_serialisation();
    serialize_object_and_back(G_OBJECT(line), line_assert_equal, FALSE, NULL);

    set_line_units_for_serialisation(line);
    serialize_object_and_back(G_OBJECT(line), line_assert_equal, FALSE, NULL);

    g_assert_finalize_object(line);
}

void
test_line_copy(void)
{
    GwyLine *line = create_line_for_serialisation();
    serializable_test_copy(GWY_SERIALIZABLE(line), line_assert_equal);

    set_line_units_for_serialisation(line);
    serializable_test_copy(GWY_SERIALIZABLE(line), line_assert_equal);

    g_assert_finalize_object(line);
}

void
test_line_assign(void)
{
    GwyLine *line = create_line_for_serialisation();
    serializable_test_assign(GWY_SERIALIZABLE(line), NULL, line_assert_equal);

    set_line_units_for_serialisation(line);
    serializable_test_assign(GWY_SERIALIZABLE(line), NULL, line_assert_equal);

    GwyLine *another = gwy_line_new(6, 14.3, FALSE);
    gwy_line_set_offset(another, -11.2);
    gwy_unit_set_from_string(gwy_line_get_unit_x(another), "V");
    gwy_unit_set_from_string(gwy_line_get_unit_y(another), "m/s");
    serializable_test_assign(GWY_SERIALIZABLE(line), GWY_SERIALIZABLE(another), line_assert_equal);
    g_assert_finalize_object(another);

    g_assert_finalize_object(line);
}

void
test_line_resize(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    GwyLine *line = gwy_line_new(1, 1.0, FALSE);
    for (guint k = 0; k < n; k++) {
        gint width = g_test_rand_int_range(1, 20);
        gwy_line_resize(line, width);
        g_assert_cmpint(gwy_line_get_res(line), ==, width);
        /* Try to crash if the memory is not allocated correctly. */
        gdouble *d = gwy_line_get_data(line);
        gwy_clear(d, width);
    }
    g_assert_finalize_object(line);
}

GwyLine*
make_i_line(void)
{
    gint res = g_test_rand_int_range(2, 20);
    GwyLine *line = gwy_line_new(res, res, FALSE);
    gdouble *d = gwy_line_get_data(line);

    for (gint i = 0; i < res; i++)
        d[i] = SOME_NUMBER*i;

    return line;
}

void
test_line_part_extract(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyLine *line = make_i_line();
        gint res = gwy_line_get_res(line);
        gint width = g_test_rand_int_range(1, res);
        gint pos = g_test_rand_int_range(0, res-width);
        GwyLine *extracted = gwy_line_part_extract(line, pos, width);
        const gdouble *d = gwy_line_get_data_const(extracted);
        for (gint i = 0; i < width; i++)
            g_assert_cmpfloat(d[i], ==, SOME_NUMBER*(i + pos));
        g_assert_finalize_object(extracted);
        g_assert_finalize_object(line);
    }
}

void
test_line_crop(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyLine *line = make_i_line();
        gint res = gwy_line_get_res(line);
        gint width = g_test_rand_int_range(1, res);
        gint pos = g_test_rand_int_range(0, res-width);
        gwy_line_crop(line, pos, width);
        const gdouble *d = gwy_line_get_data_const(line);
        for (gint i = 0; i < width; i++)
            g_assert_cmpfloat(d[i], ==, SOME_NUMBER*(i + pos));
        g_assert_finalize_object(line);
    }
}

void
test_line_part_copy(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyLine *line = make_i_line();
        gint res = gwy_line_get_res(line);
        gint len = g_test_rand_int_range(1, res);
        gint pos = g_test_rand_int_range(0, res-len+1);
        gint res2 = g_test_rand_int_range(len, 25);
        gint destpos = g_test_rand_int_range(0, res2-len+1);
        GwyLine *destline = gwy_line_new(res2, res2, FALSE);
        gwy_line_fill(destline, G_PI);
        gwy_line_part_copy(line, destline, pos, len, destpos);
        const gdouble *d = gwy_line_get_data_const(destline);
        for (gint j = 0; j < res2; j++) {
            if (j >= destpos && j-destpos < len)
                g_assert_cmpfloat(d[j], ==, SOME_NUMBER*(j + pos - destpos));
            else
                g_assert_cmpfloat(d[j], ==, G_PI);
        }
        g_assert_finalize_object(destline);
        g_assert_finalize_object(line);
    }
}

void
test_line_flip(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyLine *line = make_i_line();
        gint res = gwy_line_get_res(line);
        gwy_line_flip(line);
        const gdouble *d = gwy_line_get_data_const(line);
        for (gint i = 0; i < res; i++) {
            gint flipped_i = res-1 - i;
            g_assert_cmpfloat(d[i], ==, SOME_NUMBER*flipped_i);
        }
        g_assert_finalize_object(line);
    }
}

void
test_line_clear_part(void)
{
    const gdouble expected1[5] = { 2.0, 2.0, 0.0, 0.0, 0.0 };
    const gdouble expected2[5] = { 0.0, 0.0, 0.0, 0.0, 0.0 };
    const gdouble expected3[5] = { 0.0, 0.0, 0.0, 0.0, 2.0 };

    GwyLine *line = gwy_line_new(5, 5, FALSE);

    gwy_line_fill(line, 2.0);
    gwy_line_part_clear(line, 2, 3);
    assert_line_content(line, expected1, 5, 0.0);

    gwy_line_fill(line, 2.0);
    gwy_line_part_clear(line, 0, 5);
    assert_line_content(line, expected2, 5, 0.0);

    gwy_line_fill(line, 2.0);
    gwy_line_part_clear(line, 0, 4);
    assert_line_content(line, expected3, 5, 0.0);

    g_assert_finalize_object(line);
}

void
test_line_fill_part(void)
{
    const gdouble expected1[5] = { 2.0, 2.0, 0.0, 0.0, 0.0 };
    const gdouble expected2[5] = { 2.0, 2.0, 2.0, 2.0, 2.0 };
    const gdouble expected3[5] = { 0.0, 0.0, 0.0, 0.0, 2.0 };

    GwyLine *line = gwy_line_new(5, 5, FALSE);

    gwy_line_clear(line);
    gwy_line_part_fill(line, 0, 2, 2.0);
    assert_line_content(line, expected1, 5, 0.0);

    gwy_line_clear(line);
    gwy_line_part_fill(line, 0, 5, 2.0);
    assert_line_content(line, expected2, 5, 0.0);

    gwy_line_clear(line);
    gwy_line_part_fill(line, 4, 1, 2.0);
    assert_line_content(line, expected3, 5, 0.0);

    g_assert_finalize_object(line);
}

static void
assert_line_compatibility(gint res1, gdouble real1, const gchar *xunit1, const gchar *yunit1,
                          gint res2, gdouble real2, const gchar *xunit2, const gchar *yunit2,
                          GwyDataMismatchFlags flags_to_test,
                          GwyDataMismatchFlags expected_result)
{
    GwyLine *line1 = gwy_line_new(res1, real1, TRUE);
    if (xunit1)
        gwy_unit_set_from_string(gwy_line_get_unit_x(line1), xunit1);
    if (yunit1)
        gwy_unit_set_from_string(gwy_line_get_unit_y(line1), yunit1);

    GwyLine *line2 = gwy_line_new(res2, real2, TRUE);
    if (xunit2)
        gwy_unit_set_from_string(gwy_line_get_unit_x(line2), xunit2);
    if (yunit2)
        gwy_unit_set_from_string(gwy_line_get_unit_y(line2), yunit2);

    g_assert_cmphex(gwy_line_is_incompatible(line1, line2, flags_to_test), ==, expected_result);
    g_assert_finalize_object(line1);
    g_assert_finalize_object(line2);
}

void
test_line_compatibility_res(void)
{
    assert_line_compatibility(1, G_PI, "m", NULL,
                              1, 0.3, NULL, "A",
                              GWY_DATA_MISMATCH_RES, 0);
    assert_line_compatibility(1, G_PI, "m", NULL,
                              2, 0.3, NULL, "A",
                              GWY_DATA_MISMATCH_RES, GWY_DATA_MISMATCH_RES);
}

void
test_line_compatibility_real(void)
{
    assert_line_compatibility(1, G_PI, "m", NULL,
                              2, G_PI, NULL, "A",
                              GWY_DATA_MISMATCH_REAL, 0);
    assert_line_compatibility(1, G_PI, "m", NULL,
                              2, 8.3, NULL, "A",
                              GWY_DATA_MISMATCH_REAL, GWY_DATA_MISMATCH_REAL);
    /* Tiny differences do not break compatibility. */
    assert_line_compatibility(1, G_PI*(1.0 - 1e-14), "m", NULL,
                              2, G_PI*(1.0 + 1e-14), NULL, "A",
                              GWY_DATA_MISMATCH_REAL, 0);
}

void
test_line_compatibility_lateral(void)
{
    assert_line_compatibility(1, G_PI, NULL, NULL,
                              3, 1.0, NULL, NULL,
                              GWY_DATA_MISMATCH_LATERAL, 0);
    assert_line_compatibility(1, G_PI, NULL, "m",
                              3, 1.0, "", "A",
                              GWY_DATA_MISMATCH_LATERAL, 0);
    assert_line_compatibility(1, G_PI, "mV", "m",
                              3, 1.0, "kilovolts", "A",
                              GWY_DATA_MISMATCH_LATERAL, 0);
    assert_line_compatibility(1, G_PI, "V", "m",
                              3, 1.0, "A", "V",
                              GWY_DATA_MISMATCH_LATERAL, GWY_DATA_MISMATCH_LATERAL);
    assert_line_compatibility(1, G_PI, "N", "",
                              3, 1.0, "", "N",
                              GWY_DATA_MISMATCH_LATERAL, GWY_DATA_MISMATCH_LATERAL);
}

void
test_line_compatibility_value(void)
{
    assert_line_compatibility(1, G_PI, NULL, NULL,
                              3, 1.0, NULL, NULL,
                              GWY_DATA_MISMATCH_VALUE, 0);
    assert_line_compatibility(1, G_PI, "m", NULL,
                              3, 1.0, "A", "",
                              GWY_DATA_MISMATCH_VALUE, 0);
    assert_line_compatibility(1, G_PI, "m", "micrometres",
                              3, 1.0, "A", "pm",
                              GWY_DATA_MISMATCH_VALUE, 0);
    assert_line_compatibility(1, G_PI, "m", "A",
                              3, 1.0, "A", "V",
                              GWY_DATA_MISMATCH_VALUE, GWY_DATA_MISMATCH_VALUE);
    assert_line_compatibility(1, G_PI, "", "N",
                              3, 1.0, "N", "",
                              GWY_DATA_MISMATCH_VALUE, GWY_DATA_MISMATCH_VALUE);
}

void
test_line_compatibility_measure(void)
{
    assert_line_compatibility(1, 2e-9, "m", "g",
                              3, 6e-9, "A", "N",
                              GWY_DATA_MISMATCH_MEASURE, 0);
    assert_line_compatibility(1, 2e-9, "m", "g",
                              3, 2e-9, "A", "N",
                              GWY_DATA_MISMATCH_MEASURE, GWY_DATA_MISMATCH_MEASURE);
    assert_line_compatibility(3, 2e-9, "m", "g",
                              3, 6e-9, "A", "N",
                              GWY_DATA_MISMATCH_MEASURE, GWY_DATA_MISMATCH_MEASURE);
    /* Tiny differences do not break compatibility. */
    assert_line_compatibility(1, 2e-9*(1.0 - 1e-14), "m", "g",
                              3, 6e-9*(1.0 + 1e-14), "A", "N",
                              GWY_DATA_MISMATCH_MEASURE, 0);
}

void
test_line_compatibility_mixed(void)
{
    GwyDataMismatchFlags all_flags = (GWY_DATA_MISMATCH_RES
                                      | GWY_DATA_MISMATCH_REAL
                                      | GWY_DATA_MISMATCH_MEASURE
                                      | GWY_DATA_MISMATCH_LATERAL
                                      | GWY_DATA_MISMATCH_VALUE);
    assert_line_compatibility(1, 2e-9, "m", "V",
                              3, 2e-9, "A", "V",
                              all_flags,
                              GWY_DATA_MISMATCH_RES | GWY_DATA_MISMATCH_MEASURE | GWY_DATA_MISMATCH_LATERAL);
    assert_line_compatibility(3, 3e-9, "m", "V",
                              3, 4e-9, "A", "N",
                              all_flags,
                              GWY_DATA_MISMATCH_REAL | GWY_DATA_MISMATCH_MEASURE
                              | GWY_DATA_MISMATCH_LATERAL | GWY_DATA_MISMATCH_VALUE);
    assert_line_compatibility(3, 30e-9, NULL, NULL,
                              4, 40e-9, NULL, NULL,
                              all_flags,
                              GWY_DATA_MISMATCH_RES | GWY_DATA_MISMATCH_REAL);
}

static void
assert_line_extend(const gdouble *data, guint n,
                   gint extend_left, gint extend_right,
                   GwyExteriorType exterior, gdouble fill_value,
                   const double *extdata, guint extn,
                   gboolean keep_offsets,
                   gdouble expected_off, gdouble expected_real)
{
    GwyLine *line = gwy_line_new(n, n, FALSE);
    gwy_assign(gwy_line_get_data(line), data, n);

    GwyLine *expected = gwy_line_new(extn, expected_real, FALSE);
    gwy_assign(gwy_line_get_data(expected), extdata, extn);
    gwy_line_set_offset(expected, expected_off);

    GwyLine *extended = gwy_line_extend(line, extend_left, extend_right, exterior, fill_value, keep_offsets);
    line_assert_equal(G_OBJECT(extended), G_OBJECT(expected));

    g_assert_finalize_object(extended);
    g_assert_finalize_object(expected);
    g_assert_finalize_object(line);
}

void
test_line_extend_border(void)
{
    const gdouble data[] = { 4.0, -1.0, 0.0, G_PI };
    const gdouble extend_2_0[] = { 4.0, 4.0, 4.0, -1.0, 0.0, G_PI };
    const gdouble extend_0_2[] = { 4.0, -1.0, 0.0, G_PI, G_PI, G_PI };
    const gdouble extend_5_5[] = { 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, -1.0, 0.0, G_PI, G_PI, G_PI, G_PI, G_PI, G_PI };

    assert_line_extend(data, G_N_ELEMENTS(data), 0, 0, GWY_EXTERIOR_BORDER, G_MAXDOUBLE,
                       data, G_N_ELEMENTS(data), FALSE, 0.0, 4.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 0, 0, GWY_EXTERIOR_BORDER, G_MAXDOUBLE,
                       data, G_N_ELEMENTS(data), TRUE, 0.0, 4.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 2, 0, GWY_EXTERIOR_BORDER, G_MAXDOUBLE,
                       extend_2_0, G_N_ELEMENTS(extend_2_0), FALSE, 0.0, 6.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 2, 0, GWY_EXTERIOR_BORDER, G_MAXDOUBLE,
                       extend_2_0, G_N_ELEMENTS(extend_2_0), TRUE, -2.0, 6.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 0, 2, GWY_EXTERIOR_BORDER, G_MAXDOUBLE,
                       extend_0_2, G_N_ELEMENTS(extend_0_2), FALSE, 0.0, 6.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 0, 2, GWY_EXTERIOR_BORDER, G_MAXDOUBLE,
                       extend_0_2, G_N_ELEMENTS(extend_0_2), TRUE, 0.0, 6.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 5, 5, GWY_EXTERIOR_BORDER, G_MAXDOUBLE,
                       extend_5_5, G_N_ELEMENTS(extend_5_5), FALSE, 0.0, 14.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 5, 5, GWY_EXTERIOR_BORDER, G_MAXDOUBLE,
                       extend_5_5, G_N_ELEMENTS(extend_5_5), TRUE, -5.0, 14.0);
}

void
test_line_extend_mirror(void)
{
    const gdouble data[] = { 4.0, -1.0, 0.0, G_PI };
    const gdouble extend_2_0[] = { -1.0, 4.0, 4.0, -1.0, 0.0, G_PI };
    const gdouble extend_0_2[] = { 4.0, -1.0, 0.0, G_PI, G_PI, 0.0 };
    const gdouble extend_5_5[] = { G_PI, G_PI, 0.0, -1.0, 4.0, 4.0, -1.0, 0.0, G_PI, G_PI, 0.0, -1.0, 4.0, 4.0 };

    assert_line_extend(data, G_N_ELEMENTS(data), 0, 0, GWY_EXTERIOR_MIRROR, G_MAXDOUBLE,
                       data, G_N_ELEMENTS(data), FALSE, 0.0, 4.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 0, 0, GWY_EXTERIOR_MIRROR, G_MAXDOUBLE,
                       data, G_N_ELEMENTS(data), TRUE, 0.0, 4.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 2, 0, GWY_EXTERIOR_MIRROR, G_MAXDOUBLE,
                       extend_2_0, G_N_ELEMENTS(extend_2_0), FALSE, 0.0, 6.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 2, 0, GWY_EXTERIOR_MIRROR, G_MAXDOUBLE,
                       extend_2_0, G_N_ELEMENTS(extend_2_0), TRUE, -2.0, 6.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 0, 2, GWY_EXTERIOR_MIRROR, G_MAXDOUBLE,
                       extend_0_2, G_N_ELEMENTS(extend_0_2), FALSE, 0.0, 6.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 0, 2, GWY_EXTERIOR_MIRROR, G_MAXDOUBLE,
                       extend_0_2, G_N_ELEMENTS(extend_0_2), TRUE, 0.0, 6.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 5, 5, GWY_EXTERIOR_MIRROR, G_MAXDOUBLE,
                       extend_5_5, G_N_ELEMENTS(extend_5_5), FALSE, 0.0, 14.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 5, 5, GWY_EXTERIOR_MIRROR, G_MAXDOUBLE,
                       extend_5_5, G_N_ELEMENTS(extend_5_5), TRUE, -5.0, 14.0);
}

void
test_line_extend_periodic(void)
{
    const gdouble data[] = { 4.0, -1.0, 0.0, G_PI };
    const gdouble extend_2_0[] = { 0.0, G_PI, 4.0, -1.0, 0.0, G_PI };
    const gdouble extend_0_2[] = { 4.0, -1.0, 0.0, G_PI, 4.0, -1.0 };
    const gdouble extend_5_5[] = { G_PI, 4.0, -1.0, 0.0, G_PI, 4.0, -1.0, 0.0, G_PI, 4.0, -1.0, 0.0, G_PI, 4.0 };

    assert_line_extend(data, G_N_ELEMENTS(data), 0, 0, GWY_EXTERIOR_PERIODIC, G_MAXDOUBLE,
                       data, G_N_ELEMENTS(data), FALSE, 0.0, 4.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 0, 0, GWY_EXTERIOR_PERIODIC, G_MAXDOUBLE,
                       data, G_N_ELEMENTS(data), TRUE, 0.0, 4.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 2, 0, GWY_EXTERIOR_PERIODIC, G_MAXDOUBLE,
                       extend_2_0, G_N_ELEMENTS(extend_2_0), FALSE, 0.0, 6.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 2, 0, GWY_EXTERIOR_PERIODIC, G_MAXDOUBLE,
                       extend_2_0, G_N_ELEMENTS(extend_2_0), TRUE, -2.0, 6.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 0, 2, GWY_EXTERIOR_PERIODIC, G_MAXDOUBLE,
                       extend_0_2, G_N_ELEMENTS(extend_0_2), FALSE, 0.0, 6.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 0, 2, GWY_EXTERIOR_PERIODIC, G_MAXDOUBLE,
                       extend_0_2, G_N_ELEMENTS(extend_0_2), TRUE, 0.0, 6.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 5, 5, GWY_EXTERIOR_PERIODIC, G_MAXDOUBLE,
                       extend_5_5, G_N_ELEMENTS(extend_5_5), FALSE, 0.0, 14.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 5, 5, GWY_EXTERIOR_PERIODIC, G_MAXDOUBLE,
                       extend_5_5, G_N_ELEMENTS(extend_5_5), TRUE, -5.0, 14.0);
}

void
test_line_extend_fixed(void)
{
    const gdouble data[] = { 4.0, -1.0, 0.0, G_PI };
    const gdouble extend_2_0[] = { G_SQRT2, G_SQRT2, 4.0, -1.0, 0.0, G_PI };
    const gdouble extend_0_2[] = { 4.0, -1.0, 0.0, G_PI, G_SQRT2, G_SQRT2, };
    const gdouble extend_5_5[] = {
        G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2, 4.0, -1.0, 0.0, G_PI, G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2
    };

    assert_line_extend(data, G_N_ELEMENTS(data), 0, 0, GWY_EXTERIOR_FIXED_VALUE, G_SQRT2,
                       data, G_N_ELEMENTS(data), FALSE, 0.0, 4.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 0, 0, GWY_EXTERIOR_FIXED_VALUE, G_SQRT2,
                       data, G_N_ELEMENTS(data), TRUE, 0.0, 4.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 2, 0, GWY_EXTERIOR_FIXED_VALUE, G_SQRT2,
                       extend_2_0, G_N_ELEMENTS(extend_2_0), FALSE, 0.0, 6.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 2, 0, GWY_EXTERIOR_FIXED_VALUE, G_SQRT2,
                       extend_2_0, G_N_ELEMENTS(extend_2_0), TRUE, -2.0, 6.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 0, 2, GWY_EXTERIOR_FIXED_VALUE, G_SQRT2,
                       extend_0_2, G_N_ELEMENTS(extend_0_2), FALSE, 0.0, 6.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 0, 2, GWY_EXTERIOR_FIXED_VALUE, G_SQRT2,
                       extend_0_2, G_N_ELEMENTS(extend_0_2), TRUE, 0.0, 6.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 5, 5, GWY_EXTERIOR_FIXED_VALUE, G_SQRT2,
                       extend_5_5, G_N_ELEMENTS(extend_5_5), FALSE, 0.0, 14.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 5, 5, GWY_EXTERIOR_FIXED_VALUE, G_SQRT2,
                       extend_5_5, G_N_ELEMENTS(extend_5_5), TRUE, -5.0, 14.0);
}

void
test_line_extend_laplace(void)
{
    const gdouble data[] = { 4.0, -1.0, 0.0, G_PI };
    const gdouble extend_2_0[] = { 4.0, 4.0, 4.0, -1.0, 0.0, G_PI };
    const gdouble extend_0_2[] = { 4.0, -1.0, 0.0, G_PI, G_PI, G_PI };
    const gdouble extend_5_5[] = { 4.0, 4.0, 4.0, 4.0, 4.0, 4.0, -1.0, 0.0, G_PI, G_PI, G_PI, G_PI, G_PI, G_PI };

    assert_line_extend(data, G_N_ELEMENTS(data), 0, 0, GWY_EXTERIOR_LAPLACE, G_MAXDOUBLE,
                       data, G_N_ELEMENTS(data), FALSE, 0.0, 4.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 0, 0, GWY_EXTERIOR_LAPLACE, G_MAXDOUBLE,
                       data, G_N_ELEMENTS(data), TRUE, 0.0, 4.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 2, 0, GWY_EXTERIOR_LAPLACE, G_MAXDOUBLE,
                       extend_2_0, G_N_ELEMENTS(extend_2_0), FALSE, 0.0, 6.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 2, 0, GWY_EXTERIOR_LAPLACE, G_MAXDOUBLE,
                       extend_2_0, G_N_ELEMENTS(extend_2_0), TRUE, -2.0, 6.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 0, 2, GWY_EXTERIOR_LAPLACE, G_MAXDOUBLE,
                       extend_0_2, G_N_ELEMENTS(extend_0_2), FALSE, 0.0, 6.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 0, 2, GWY_EXTERIOR_LAPLACE, G_MAXDOUBLE,
                       extend_0_2, G_N_ELEMENTS(extend_0_2), TRUE, 0.0, 6.0);

    assert_line_extend(data, G_N_ELEMENTS(data), 5, 5, GWY_EXTERIOR_LAPLACE, G_MAXDOUBLE,
                       extend_5_5, G_N_ELEMENTS(extend_5_5), FALSE, 0.0, 14.0);
    assert_line_extend(data, G_N_ELEMENTS(data), 5, 5, GWY_EXTERIOR_LAPLACE, G_MAXDOUBLE,
                       extend_5_5, G_N_ELEMENTS(extend_5_5), TRUE, -5.0, 14.0);
}

void
test_line_extend_connect(void)
{
    /* The only thing we can test exactly is that it reproduces constants because otherwise the behaviour is
     * implementation-defined. */
    gdouble data[34];
    for (gint k = 0; k < 34; k++)
        data[k] = 1.0;

    for (gint len = 1; len < 10; len++) {
        for (gint left = 0; left < 12; left++) {
            for (gint right = 0; right < 12; right++) {
                assert_line_extend(data, len, left, right, GWY_EXTERIOR_CONNECT, G_MAXDOUBLE,
                                   data, len + left + right, FALSE, 0.0, len + left + right);
            }
        }
    }
}

static GwyLine*
make_random_linear_line(gdouble *pa, gdouble *pb)
{
    gint res = g_test_rand_int_range(2, 20);
    gdouble dx = exp(2*g_test_rand_double() - 1);
    GwyLine *line = gwy_line_new(res, res*dx, TRUE);
    gdouble a = g_test_rand_double_range(-5.0, 5.0);
    gdouble b = g_test_rand_double_range(-1.0, 1.0);
    gwy_line_line_level(line, -a, -b);

    if (pa)
        *pa = a;
    if (pb)
        *pb = b;

    return line;
}

void
test_line_extend_undefined(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyLine *line = make_random_linear_line(NULL, NULL);
        gint left = g_test_rand_int_range(0, 20);
        gint right = g_test_rand_int_range(0, 20);
        gint res = gwy_line_get_res(line);
        GwyLine *extended = gwy_line_extend(line, left, right,
                                            GWY_EXTERIOR_UNDEFINED, G_MAXDOUBLE, FALSE);
        GwyLine *extracted = gwy_line_part_extract(extended, left, res);
        /* There may be rounding errors in the real size, unfortunately. Test it with an ε and then just make the two
         * dimensions exactly equal. */
        g_assert_cmpfloat_with_epsilon(gwy_line_get_real(extracted), gwy_line_get_real(line),
                                       2*(res + left + right)*DBL_EPSILON);
        gwy_line_set_real(extracted, gwy_line_get_real(line));
        line_assert_equal(G_OBJECT(extracted), G_OBJECT(line));

        g_assert_finalize_object(line);
        g_assert_finalize_object(extracted);
        g_assert_finalize_object(extended);
    }
}

void
test_line_cumulate_plain(void)
{
    const gdouble data[] = { 4.0, -1.0, 0.0, G_PI, G_PI };
    const gdouble cdata[] = { 4.0, 3.0, 3.0, 3.0 + G_PI, 3.0 + 2*G_PI };
    guint n = G_N_ELEMENTS(data);

    GwyLine *line = gwy_line_new(n, 0.3, FALSE);
    gwy_line_set_offset(line, 5.0);
    gwy_unit_set_from_string(gwy_line_get_unit_x(line), "m");
    gwy_unit_set_from_string(gwy_line_get_unit_y(line), "A");

    gwy_assign(gwy_line_get_data(line), data, n);
    gwy_line_cumulate(line, FALSE);

    assert_line_content(line, cdata, n, 3*DBL_EPSILON);
    g_assert_true(gwy_unit_equal_string(gwy_line_get_unit_x(line), "m"));
    g_assert_true(gwy_unit_equal_string(gwy_line_get_unit_y(line), "A"));

    g_assert_finalize_object(line);
}

void
test_line_cumulate_to_cdist(void)
{
    /* These must integrate to 1 for the call to be valid. */
    const gdouble data[] = { 0.0, 0.0, 0.0, 1.0, 0.0, 2.0, 3.0, 0.0, 1.0, 1.0 };
    const gdouble cdata[] = { 0.0, 0.0, 0.0, 0.125, 0.125, 3*0.125, 6*0.125, 6*0.125, 7*0.125, 1.0 };
    guint n = G_N_ELEMENTS(data);

    GwyLine *line = gwy_line_new(n, 0.125 * 10, FALSE);
    gwy_line_set_offset(line, 5.0);
    gwy_unit_set_from_string(gwy_line_get_unit_x(line), "m");
    gwy_unit_set_from_string(gwy_line_get_unit_y(line), "A");

    gwy_assign(gwy_line_get_data(line), data, n);
    gwy_line_cumulate(line, TRUE);

    assert_line_content(line, cdata, n, 3*DBL_EPSILON);
    g_assert_true(gwy_unit_equal_string(gwy_line_get_unit_x(line), "m"));
    g_assert_true(gwy_unit_equal_string(gwy_line_get_unit_y(line), "m A"));

    g_assert_finalize_object(line);
}

/* NB: Stepping stone.
 *
 * Assuming gwy_line_line_level() is correct, we then use it to make all kinds of linear lines with known
 * properties. */
void
test_line_line_level(void)
{
    enum { res1 = 5 };
    GwyLine *line1 = gwy_line_new(res1, 6.0, TRUE);
    gwy_line_line_level(line1, -G_PI, 2.0);
    const gdouble expected1[res1] = {
        G_PI, G_PI - 2.0, G_PI - 2*2.0, G_PI - 3*2.0, G_PI - 4*2.0,
    };
    /* These should probably be exact. But a few ε is OK. */
    assert_line_content(line1, expected1, res1, 4*DBL_EPSILON);
    g_assert_finalize_object(line1);
}

void
test_line_fit_line_random(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, b;
        GwyLine *line = make_random_linear_line(&a, &b);
        gint res = gwy_line_get_res(line);
        gdouble fita, fitb;
        gwy_line_fit_line(line, &fita, &fitb);
        gdouble s = fmax(fabs(a), fabs(b));
        g_assert_cmpfloat_with_epsilon(fita, a, 4*DBL_EPSILON*res*s);
        g_assert_cmpfloat_with_epsilon(fitb, b, DBL_EPSILON*res*s);
        gwy_line_line_level(line, a, b);
        g_assert_cmpfloat(fabs(gwy_line_min(line)), <, 2*DBL_EPSILON*res);
        g_assert_cmpfloat(fabs(gwy_line_max(line)), <, 2*DBL_EPSILON*res);
        g_assert_finalize_object(line);
    }
}

void
test_line_fit_line_degenerate(void)
{
    gdouble fita, fitb;

    const gdouble data1[1] = { 4.0 };
    GwyLine *line1 = gwy_line_new(1, 1.0, FALSE);
    gwy_assign(gwy_line_get_data(line1), data1, 1);
    gwy_line_fit_line(line1, &fita, &fitb);
    g_assert_cmpfloat_with_epsilon(fita, 4.0, 3*DBL_EPSILON);
    g_assert_cmpfloat(fitb, ==, 0.0);
    g_assert_finalize_object(line1);
}

void
test_line_stats_avg_linear(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, b;
        GwyLine *line = make_random_linear_line(&a, &b);
        gint res = gwy_line_get_res(line);
        gdouble expected = a + b*(res - 1.0)/2.0;
        g_assert_cmpfloat_with_epsilon(gwy_line_mean(line), expected, 4*DBL_EPSILON*res);
        g_assert_finalize_object(line);
    }
}

void
test_line_stats_sum_linear(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, b;
        GwyLine *line = make_random_linear_line(&a, &b);
        gint res = gwy_line_get_res(line);
        gdouble expected = res*(a + b*(res - 1.0)/2.0);
        g_assert_cmpfloat_with_epsilon(gwy_line_sum(line), expected, 3*DBL_EPSILON*res*res);
        g_assert_finalize_object(line);
    }
}

void
test_line_stats_min_linear(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, b;
        GwyLine *line = make_random_linear_line(&a, &b);
        gint res = gwy_line_get_res(line);
        gdouble expected = fmin(a, a + b*(res - 1.0));
        g_assert_cmpfloat_with_epsilon(gwy_line_min(line), expected, 2*DBL_EPSILON*res);
        g_assert_finalize_object(line);
    }
}

void
test_line_stats_max_linear(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, b;
        GwyLine *line = make_random_linear_line(&a, &b);
        gint res = gwy_line_get_res(line);
        gdouble expected = fmax(a, a + b*(res - 1.0));
        g_assert_cmpfloat_with_epsilon(gwy_line_max(line), expected, 2*DBL_EPSILON*res);
        g_assert_finalize_object(line);
    }
}

void
test_line_stats_rms_linear(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, b;
        GwyLine *line = make_random_linear_line(&a, &b);
        gint res = gwy_line_get_res(line);
        gint x2m1 = res*res - 1;
        gdouble expected = sqrt(b*b*x2m1/12.0);
        expected *= sqrt(res/(res - 1.0));   // Degrees of freedom
        g_assert_cmpfloat_with_epsilon(gwy_line_rms(line), expected, 2*DBL_EPSILON*res);
        g_assert_finalize_object(line);
    }
}

void
test_line_stats_skew_linear(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, b;
        GwyLine *line = make_random_linear_line(&a, &b);
        gint res = gwy_line_get_res(line);
        gdouble expected = 0.0;
        /* Use a large tolerance. We are getting relatively large errors, for not very clear reasons. Sure, for linear
         * line data, the natural summation order of (y_i - y_mean)³ is about the worst possible for rounding errors.
         * Is it the entire reason? */
        g_assert_cmpfloat_with_epsilon(gwy_line_skew(line), expected, 1000*DBL_EPSILON*res);
        g_assert_finalize_object(line);
    }
}

void
test_line_stats_kurtosis_linear(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, b;
        GwyLine *line = make_random_linear_line(&a, &b);
        gint res = gwy_line_get_res(line);
        gint x2m1 = res*res - 1;
        gdouble t = b*b*x2m1;
        gdouble m2 = t/12.0;
        gdouble m4 = b*b*t*(res*res - 7.0/3.0)/80.0;
        gdouble expected = m4/(m2*m2) - 3.0;
        g_assert_cmpfloat_with_epsilon(gwy_line_kurtosis(line), expected, 500*DBL_EPSILON*res);
        g_assert_finalize_object(line);
    }
}

void
test_line_stats_variation_linear(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, b;
        GwyLine *line = make_random_linear_line(&a, &b);
        gint res = gwy_line_get_res(line);
        /* This may looks weird – and we got it wrong at one point. The expected value is |β|L, where β is the line
         * coefficient in real(!) coordinates β = dy/dx. We have instead b = dy/dn = dy/dx × dx/dn = β × L/n.
         * So, |β|L = |b|n/L × L = |b|n. */
        gdouble expected = fabs(b)*res;
        g_assert_cmpfloat_with_epsilon(gwy_line_variation(line), expected, 6*DBL_EPSILON*res);
        g_assert_finalize_object(line);
    }
}

void
test_line_stats_tan_beta0_linear(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, b;
        GwyLine *line = make_random_linear_line(&a, &b);
        gint res = gwy_line_get_res(line);
        gdouble expected = fabs(b)/gwy_line_get_dx(line);
        g_assert_cmpfloat_with_epsilon(gwy_line_tan_beta0(line), expected, 12*DBL_EPSILON*res);
        g_assert_finalize_object(line);
    }
}

void
test_line_stats_length_linear(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, b;
        GwyLine *line = make_random_linear_line(&a, &b);
        gint res = gwy_line_get_res(line);
        gdouble dx = gwy_line_get_dx(line);
        gdouble expected = sqrt(1.0 + b*b/(dx*dx))*gwy_line_get_real(line);
        g_assert_cmpfloat_with_epsilon(gwy_line_length(line), expected, 20*DBL_EPSILON*res);
        g_assert_finalize_object(line);
    }
}

static GwyLine*
make_line_with_peaks(guint nsegments, gdouble *factors)
{
    const gdouble data[] = {
        /* Fourth highest peak. */
        0.0, 1.2,
        /* Fourth deepest valley. */
        0.0, -1.2,
        /* Highest peak */
        0.0, 1.9, 0.3, 2.0,
        /* Second deepest valley */
        0.0, -0.8, -0.2, -1.4, -1.6,
        /* Third highest peak */
        1.25,
        /* Non-valley/non-peak */
        0.5, 0.0, -0.3, 0.5, -0.2, 0.0,
        /* Third highest peak, continuing (it did not go below the valley threshold). */
        1.3,
        /* Deepest valley */
        -2.5,
        /* Second highest peak */
        1.5, 1.3, 1.4,
        /* Non-valley/non-peak */
        -0.2, 0.0, 0.7, 0.35, -0.3, 0.0,
        /* Third deepest valley. */
        -1.2, -1.25, -1.2,
        /* Non-valley/non-peak */
        -0.5, 0.0, 0.5, -0.5, 0.0,
        /* Third deepest valley, continued. */
        -1.35, 0.0,
    };
    const guint n1 = G_N_ELEMENTS(data);

    /* Replicate the same profile m times, but fudge it a bit with random factors to disturb coincidences in the
     * fixed numbers above. */
    for (guint mm = 0; mm < nsegments; mm++)
        factors[mm] = g_test_rand_double_range(0.95, 1.05);

    GwyLine *line = gwy_line_new(n1*nsegments, n1*nsegments, FALSE);
    gdouble *d = gwy_line_get_data(line);
    for (guint mm = 0; mm < nsegments; mm++) {
        for (guint j = 0; j < n1; j++)
            d[mm*n1 + j] = data[j]*factors[mm];
    }

    return line;
}

/* We cannot really have |peaks| ≠ |valleys| because valleys have to be separated by peaks and vice versa. They
 * can differ at most by one, which then breaks when the segments are stitched. */
static const gdouble expected_peaks[4] = { 2.0, 1.5, 1.3, 1.2 };
static const gdouble expected_valleys[4] = { 2.5, 1.6, 1.35, 1.2 };
static const guint npeaks = G_N_ELEMENTS(expected_peaks);
static const guint nvalleys = G_N_ELEMENTS(expected_valleys);

void
test_line_peaks_count_peaks(void)
{
    guint n = g_test_slow() ? 100 : 20;

    for (guint i = 0; i < n; i++) {
        guint m = g_test_rand_int_range(1, 8);
        gdouble factors[m];
        GwyLine *line = make_line_with_peaks(m, factors);

        g_assert_cmpuint(gwy_line_count_peaks(line, TRUE, 1.0, 1.0), ==, npeaks*m);
        g_assert_finalize_object(line);
    }
}

void
test_line_peaks_count_valleys(void)
{
    guint n = g_test_slow() ? 100 : 20;

    for (guint i = 0; i < n; i++) {
        guint m = g_test_rand_int_range(1, 8);
        gdouble factors[m];
        GwyLine *line = make_line_with_peaks(m, factors);

        g_assert_cmpuint(gwy_line_count_peaks(line, TRUE, 1.0, 1.0), ==, nvalleys*m);
        g_assert_finalize_object(line);
    }
}

void
test_line_peaks_kth_peaks_basic(void)
{
    guint n = g_test_slow() ? 100 : 20;

    for (guint i = 0; i < n; i++) {
        guint m = g_test_rand_int_range(1, 8);
        gdouble factors[m], peaks[m];
        GwyLine *line = make_line_with_peaks(m, factors);

        for (guint k = 0; k < npeaks; k++) {
            gwy_line_kth_peaks(line, m, k+1, TRUE, FALSE, 1.0, 1.0, peaks);
            for (guint mm = 0; mm < m; mm++)
                g_assert_cmpfloat_with_epsilon(peaks[mm], expected_peaks[k]*factors[mm], 4*DBL_EPSILON);
        }

        g_assert_finalize_object(line);
    }
}

void
test_line_peaks_kth_valleys_basic(void)
{
    guint n = g_test_slow() ? 100 : 20;

    for (guint i = 0; i < n; i++) {
        guint m = g_test_rand_int_range(1, 8);
        gdouble factors[m], valleys[m];
        GwyLine *line = make_line_with_peaks(m, factors);

        for (guint k = 0; k < nvalleys; k++) {
            gwy_line_kth_peaks(line, m, k+1, FALSE, FALSE, 1.0, 1.0, valleys);
            for (guint mm = 0; mm < m; mm++)
                g_assert_cmpfloat_with_epsilon(valleys[mm], expected_valleys[k]*factors[mm], 4*DBL_EPSILON);
        }

        g_assert_finalize_object(line);
    }
}

void
test_line_peaks_kth_peaks_average(void)
{
    guint n = g_test_slow() ? 100 : 20;

    for (guint i = 0; i < n; i++) {
        guint m = g_test_rand_int_range(1, 8);
        gdouble factors[m], peaks[m];
        GwyLine *line = make_line_with_peaks(m, factors);

        for (guint k = 0; k < npeaks; k++) {
            gwy_line_kth_peaks(line, m, k+1, TRUE, TRUE, 1.0, 1.0, peaks);
            gdouble expected = gwy_math_mean(expected_peaks, k+1);
            for (guint mm = 0; mm < m; mm++)
                g_assert_cmpfloat_with_epsilon(peaks[mm], expected*factors[mm], 4*DBL_EPSILON);
        }

        g_assert_finalize_object(line);
    }
}

void
test_line_peaks_kth_valleys_average(void)
{
    guint n = g_test_slow() ? 100 : 20;

    for (guint i = 0; i < n; i++) {
        guint m = g_test_rand_int_range(1, 8);
        gdouble factors[m], valleys[m];
        GwyLine *line = make_line_with_peaks(m, factors);

        for (guint k = 0; k < nvalleys; k++) {
            gwy_line_kth_peaks(line, m, k+1, FALSE, TRUE, 1.0, 1.0, valleys);
            gdouble expected = gwy_math_mean(expected_valleys, k+1);
            for (guint mm = 0; mm < m; mm++)
                g_assert_cmpfloat_with_epsilon(valleys[mm], expected*factors[mm], 4*DBL_EPSILON);
        }

        g_assert_finalize_object(line);
    }
}

void
test_line_peaks_kth_peaks_too_few(void)
{
    guint n = g_test_slow() ? 100 : 20;

    for (guint i = 0; i < n; i++) {
        guint m = g_test_rand_int_range(1, 8);
        gdouble factors[m], peaks[m];
        GwyLine *line = make_line_with_peaks(m, factors);

        gwy_line_kth_peaks(line, m, npeaks+1, TRUE, FALSE, 1.0, 1.0, peaks);
        for (guint mm = 0; mm < m; mm++)
            g_assert_cmpfloat_with_epsilon(peaks[mm], expected_peaks[npeaks-1]*factors[mm], 4*DBL_EPSILON);

        g_assert_finalize_object(line);
    }
}

void
test_line_peaks_kth_valleys_too_few(void)
{
    guint n = g_test_slow() ? 100 : 20;

    for (guint i = 0; i < n; i++) {
        guint m = g_test_rand_int_range(1, 8);
        gdouble factors[m], valleys[m];
        GwyLine *line = make_line_with_peaks(m, factors);

        gwy_line_kth_peaks(line, m, nvalleys+1, FALSE, FALSE, 1.0, 1.0, valleys);
        for (guint mm = 0; mm < m; mm++)
            g_assert_cmpfloat_with_epsilon(valleys[mm], expected_valleys[nvalleys-1]*factors[mm], 4*DBL_EPSILON);

        g_assert_finalize_object(line);
    }
}

void
test_line_peaks_kth_peaks_none(void)
{
    guint n = g_test_slow() ? 100 : 20;

    for (guint i = 0; i < n; i++) {
        guint m = g_test_rand_int_range(1, 8);
        gdouble factors[m], peaks[m];
        GwyLine *line = make_line_with_peaks(m, factors);

        gwy_line_kth_peaks(line, m, g_test_rand_int_range(1, 5), TRUE, FALSE, 10.0, 10.0, peaks);
        for (guint mm = 0; mm < m; mm++)
            g_assert_cmpfloat(peaks[mm], <, 0.0);

        g_assert_finalize_object(line);
    }
}

void
test_line_peaks_kth_valleys_none(void)
{
    guint n = g_test_slow() ? 100 : 20;

    for (guint i = 0; i < n; i++) {
        guint m = g_test_rand_int_range(1, 8);
        gdouble factors[m], valleys[m];
        GwyLine *line = make_line_with_peaks(m, factors);

        gwy_line_kth_peaks(line, m, g_test_rand_int_range(1, 5), FALSE, FALSE, 10.0, 10.0, valleys);
        for (guint mm = 0; mm < m; mm++)
            g_assert_cmpfloat(valleys[mm], <, 0.0);

        g_assert_finalize_object(line);
    }
}

void
test_line_laplace_solve(void)
{
    guint n = g_test_slow() ? 100 : 20;

    for (guint i = 0; i < n; i++) {
        gint res = g_test_rand_int_range(2, 40);
        GwyLine *line = gwy_line_new(res, res, FALSE);
        GwyLine *mask_line = gwy_line_new(res, res, FALSE);
        gdouble *d = gwy_line_get_data(line);
        gdouble *m = gwy_line_get_data(mask_line);
        for (gint k = 0; k < res; k++) {
            d[k] = g_test_rand_double();
            m[k] = g_test_rand_int_range(0, 3);
        }
        GwyLine *interpolated = gwy_line_copy(line);
        gwy_line_laplace_solve(interpolated, mask_line);

        const gdouble *r = gwy_line_get_data(interpolated);
        for (gint k = 0; k < res; k++) {
            if (m[k] > 0.0) {
                gint nn = 0;
                gdouble sn = 0.0;
                if (k) {
                    sn += r[k-1];
                    nn++;
                }
                if (k < res-1) {
                    sn += r[k+1];
                    nn++;
                }
                gdouble nnavg = sn/nn;
                g_assert_cmpfloat_with_epsilon(r[k], nnavg, 1e-14);
            }
            else {
                g_assert_cmpfloat(r[k], ==, d[k]);
            }
        }

        g_assert_finalize_object(interpolated);
        g_assert_finalize_object(mask_line);
        g_assert_finalize_object(line);
    }
}

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
