package dev.dtrentin.chart.render import dev.dtrentin.chart.model.ChartConfig import dev.dtrentin.chart.model.DataConfig import dev.dtrentin.chart.model.YRange import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class AxisRendererTest { // T9: resolveYRange writes into out param (Fixed range). @Test fun resolveYRange_writesIntoOutArray_fixed() { val cfg = ChartConfig(data = DataConfig(yRange = YRange.Fixed(min = -5f, max = 12f))) val out = FloatArray(2) AxisRenderer.resolveYRange(cfg, dataMin = -100f, dataMax = 100f, outYRange = out) assertEquals(-5f, out[0]) assertEquals(12f, out[1]) } // T9: resolveYRange writes into out param (Auto with padding). @Test fun resolveYRange_writesIntoOutArray_autoWithPadding() { val cfg = ChartConfig(data = DataConfig(yRange = YRange.Auto(paddingFraction = 0.1f))) val out = FloatArray(2) AxisRenderer.resolveYRange(cfg, dataMin = 0f, dataMax = 10f, outYRange = out) // padding = (10 - 0) * 0.1 = 1 → expect [-1, 11]. assertEquals(-1f, out[0]) assertEquals(11f, out[1]) } // T9: resolveYRange handles flat data — uses range floor 1e-6 to avoid div-by-zero. // With paddingFraction 0.1 and floor 1e-6, the effective padding is 1e-7. Below Float // resolution at value 5f, so out[0] == out[1] == 5f is permitted (downstream renderers // re-apply their own (yMax-yMin).coerceAtLeast(1e-6f)). Just assert no NaN/Infinity. @Test fun resolveYRange_flatData_writesFiniteValues() { val cfg = ChartConfig(data = DataConfig(yRange = YRange.Auto(paddingFraction = 0.1f))) val out = FloatArray(2) AxisRenderer.resolveYRange(cfg, dataMin = 5f, dataMax = 5f, outYRange = out) assertTrue(out[0].isFinite()) assertTrue(out[1].isFinite()) assertTrue(out[1] >= out[0]) } // T-split: stabilizeYRange snaps outward to the tick grid and CONTAINS the exact range. @Test fun stabilizeYRange_snapsToTicks_andContainsExactRange() { val out = FloatArray(2) // range = 9.1 → niceInterval(9.1, 5) = 2 → floor(0.3/2)*2 = 0, ceil(9.4/2)*2 = 10. AxisRenderer.stabilizeYRange(0.3f, 9.4f, out) assertEquals(0f, out[0]) assertEquals(10f, out[1]) assertTrue(out[0] <= 0.3f && out[1] >= 9.4f) } // T-split: small frame-to-frame wobble inside a tick band → IDENTICAL bounds (cold stays cold). @Test fun stabilizeYRange_stableUnderSmallWobble() { val a = FloatArray(2) val b = FloatArray(2) AxisRenderer.stabilizeYRange(0.31f, 9.38f, a) AxisRenderer.stabilizeYRange(0.34f, 9.42f, b) assertEquals(a[0], b[0]) assertEquals(a[1], b[1]) } // T-split: empty / non-positive range passes through unchanged (no NaN/Inf). @Test fun stabilizeYRange_passthroughForEmptyRange() { val out = FloatArray(2) AxisRenderer.stabilizeYRange(5f, 5f, out) assertEquals(5f, out[0]) assertEquals(5f, out[1]) } // T-split: negative range spanning zero snaps symmetrically outward. @Test fun stabilizeYRange_negativeRange_snapsOutward() { val out = FloatArray(2) // range = 2.4 → niceInterval(2.4, 5) = 0.5 → floor(-1.2/0.5)*0.5 = -1.5, ceil(1.2/0.5)*0.5 = 1.5. AxisRenderer.stabilizeYRange(-1.2f, 1.2f, out) assertEquals(-1.5f, out[0]) assertEquals(1.5f, out[1]) assertTrue(out[0] <= -1.2f && out[1] >= 1.2f) } }