package org.vaadin.svgvis;

import org.junit.jupiter.api.Test;
import org.vaadin.svgvis.SvgSparkLine.DataPoint;

import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class SparkLineGeometryTest {

    /** A reading every second, a rise at the very end, and one spike in the middle. */
    private static List<DataPoint> readings() {
        List<DataPoint> data = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            double y = 20 + Math.sin(i / 50.0) * 0.2;
            if (i == 500) y = 30;
            if (i > 990) y = 20 + (i - 990) * 0.5;
            data.add(new DataPoint(i, y));
        }
        return data;
    }

    @Test
    void lttbKeepsTheLatestReadingAndThePeaks() {
        List<DataPoint> data = readings();
        List<DataPoint> sampled = SparkLineGeometry.lttb(data, 100);
        assertEquals(100, sampled.size());
        assertEquals(data.getFirst(), sampled.getFirst());
        assertEquals(data.getLast(), sampled.getLast(), "the curve ends at the latest reading, not an average");
        assertTrue(sampled.stream().anyMatch(p -> p.y() == 30), "the spike is an actual point that survives");
        assertTrue(data.containsAll(sampled), "only actual readings, no averages");
    }

    /** Noise averaged away, yet the curve ends at the latest reading and shows its rise. */
    @Test
    void theMovingAverageReachesTheLatestReading() {
        List<DataPoint> data = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            double noise = (i % 2 == 0 ? 0.1 : -0.1);
            double rise = i >= 980 ? (i - 980) * 0.1 : 0;
            data.add(new DataPoint(i / 999.0, 20 + noise + rise));
        }
        List<DataPoint> averaged = SparkLineGeometry.movingAverage(data, 50);
        assertEquals(1.0, averaged.getLast().x(), 1e-9, "the curve reaches the latest reading");
        assertEquals(0.0, averaged.getFirst().x(), 1e-9, "and starts at the first");
        for (DataPoint p : averaged.subList(1, averaged.size() - 5)) {
            assertEquals(20, p.y(), 0.05, "the noise is averaged away");
        }
        DataPoint lastBucket = averaged.get(averaged.size() - 2);
        assertTrue(averaged.getLast().y() > lastBucket.y(), "the end turns with the rise before a whole bucket shows it");
        assertTrue(averaged.getLast().y() > 21, "the end is " + averaged.getLast().y());
    }

    @Test
    void lttbLeavesShortDataAlone() {
        List<DataPoint> data = readings().subList(0, 50);
        assertEquals(data, SparkLineGeometry.lttb(data, 100));
    }

    /** Dense readings, a long gap and dense readings again: the case that looped. */
    private static List<double[]> gappy() {
        List<double[]> points = new ArrayList<>();
        for (int i = 0; i < 10; i++) points.add(new double[]{i * 2, 50 + (i % 2) * 5});
        for (int i = 0; i < 10; i++) points.add(new double[]{900 + i * 2, 20 + (i % 3) * 4});
        return points;
    }

    @Test
    void aLongIntervalSplitsTheRun() {
        List<List<double[]>> runs = SparkLineGeometry.splitAtGaps(gappy());
        assertEquals(2, runs.size());
        assertEquals(10, runs.get(0).size());
        assertEquals(10, runs.get(1).size());
    }

    /** A fast burst, a gap, then readings at a slower pace: the slower pace is not a gap. */
    @Test
    void aChangeOfPaceIsNotAGap() {
        List<double[]> points = new ArrayList<>();
        for (int i = 0; i < 40; i++) points.add(new double[]{i * 0.2, 50});
        for (int i = 0; i < 6; i++) points.add(new double[]{990 + i * 2.4, 40 + i});
        List<List<double[]>> runs = SparkLineGeometry.splitAtGaps(points);
        assertEquals(2, runs.size(), "only the long silence splits");
        assertEquals(6, runs.get(1).size());
    }

    @Test
    void oneMissedReadingIsNotAGap() {
        List<double[]> points = new ArrayList<>();
        for (int i = 0; i < 30; i++) if (i != 15) points.add(new double[]{i * 10, i % 4});
        assertEquals(1, SparkLineGeometry.splitAtGaps(points).size());
    }

    /** What LTTB keeps is unevenly spaced; gaps found before downsampling are the real ones. */
    @Test
    void downsampledPointsKeepOnlyTheRealGaps() {
        List<DataPoint> data = readings();
        List<double[]> raw = data.stream().map(p -> new double[]{p.x(), p.y()}).toList();
        List<double[]> gaps = SparkLineGeometry.gaps(raw);
        assertEquals(0, gaps.size(), "a reading every second has no gaps");
        List<double[]> sampled = SparkLineGeometry.lttb(data, 40).stream()
                .map(p -> new double[]{p.x(), p.y()}).toList();
        assertEquals(1, SparkLineGeometry.splitAtGaps(sampled, gaps).size());
    }

    @Test
    void evenReadingsAreOneRun() {
        List<double[]> points = new ArrayList<>();
        for (int i = 0; i < 50; i++) points.add(new double[]{i * 10, i % 7});
        assertEquals(1, SparkLineGeometry.splitAtGaps(points).size());
    }

    @Test
    void theCurveStaysWithinEachSegment() {
        // Uneven spacing inside one run, as the old curve's control points overshot on
        List<double[]> points = List.of(
                new double[]{0, 50}, new double[]{1, 52}, new double[]{2, 49},
                new double[]{300, 30}, new double[]{301, 35}, new double[]{302, 31});
        List<double[]> curve = SparkLineGeometry.monotoneCurve(points);
        assertEquals(points.size() - 1, curve.size());
        for (int i = 0; i < curve.size(); i++) {
            double[] from = points.get(i);
            double[] to = points.get(i + 1);
            double[] c = curve.get(i);
            for (double x : new double[]{c[0], c[2]}) {
                assertTrue(x >= from[0] && x <= to[0], "control point x " + x + " outside segment " + i);
            }
            double low = Math.min(from[1], to[1]);
            double high = Math.max(from[1], to[1]);
            for (double y : new double[]{c[1], c[3]}) {
                assertTrue(y >= low - 1e-9 && y <= high + 1e-9, "control point y " + y + " overshoots segment " + i);
            }
            assertEquals(to[0], c[4]);
            assertEquals(to[1], c[5]);
        }
    }
}
