package dev.dtrentin.chart.interaction import androidx.compose.ui.graphics.Color import dev.dtrentin.chart.SignalEntry import dev.dtrentin.chart.buffer.TieredBuffer import dev.dtrentin.chart.model.SignalConfig import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class InverseProjectionTest { // ── pixelXToTimestampMs ─────────────────────────────────────────────────── @Test fun pixelXToTimestampMs_leftEdge() { val ts = InverseProjection.pixelXToTimestampMs( pixelX = 0f, chartLeft = 0f, chartRight = 1000f, windowStartMs = 100_000L, windowMs = 10_000L, ) assertEquals(100_000L, ts) } @Test fun pixelXToTimestampMs_rightEdge() { val ts = InverseProjection.pixelXToTimestampMs( pixelX = 1000f, chartLeft = 0f, chartRight = 1000f, windowStartMs = 100_000L, windowMs = 10_000L, ) assertEquals(110_000L, ts) } @Test fun pixelXToTimestampMs_middle() { val ts = InverseProjection.pixelXToTimestampMs( pixelX = 500f, chartLeft = 0f, chartRight = 1000f, windowStartMs = 100_000L, windowMs = 10_000L, ) assertEquals(105_000L, ts) } @Test fun pixelXToTimestampMs_clampsBelowChartLeft() { val ts = InverseProjection.pixelXToTimestampMs( pixelX = -50f, chartLeft = 0f, chartRight = 1000f, windowStartMs = 100_000L, windowMs = 10_000L, ) assertEquals(100_000L, ts) // clamped to left edge } @Test fun pixelXToTimestampMs_clampsAboveChartRight() { val ts = InverseProjection.pixelXToTimestampMs( pixelX = 5000f, chartLeft = 0f, chartRight = 1000f, windowStartMs = 100_000L, windowMs = 10_000L, ) assertEquals(110_000L, ts) // clamped to right edge } @Test fun pixelXToTimestampMs_withChartLeftInset() { // chartLeft=52 (Y-axis label inset), chartRight=552 → 500px chart area. val ts = InverseProjection.pixelXToTimestampMs( pixelX = 302f, // 250 px into chart area = 50% chartLeft = 52f, chartRight = 552f, windowStartMs = 200_000L, windowMs = 4_000L, ) assertEquals(202_000L, ts) } @Test fun pixelXToTimestampMs_degenerateChartWidth_returnsWindowStart() { // chartRight == chartLeft → coerced to width 1; frac clamped to [0,1]. val ts = InverseProjection.pixelXToTimestampMs( pixelX = 100f, chartLeft = 100f, chartRight = 100f, windowStartMs = 50_000L, windowMs = 1000L, ) // pixelX - chartLeft = 0 → frac = 0 → windowStartMs. assertEquals(50_000L, ts) } // ── nearestSampleValue ──────────────────────────────────────────────────── private fun newEntry(): SignalEntry = SignalEntry(SignalConfig(color = Color.Red), TieredBuffer()) /** Populate [entry] scratch arrays directly with [samples] (ts, v). */ private fun populate(entry: SignalEntry, samples: List>) { for ((i, s) in samples.withIndex()) { entry.scratchTs[i] = s.first entry.scratchV[i] = s.second } entry.scratchCount = samples.size } @Test fun nearestSampleValue_exactMatch() { val e = newEntry() populate(e, listOf(100L to 1f, 200L to 2f, 300L to 3f)) assertEquals(2f, InverseProjection.nearestSampleValue(e, 200L)) } @Test fun nearestSampleValue_betweenSamples_picksNearer() { val e = newEntry() populate(e, listOf(100L to 1f, 200L to 2f, 300L to 3f)) assertEquals(2f, InverseProjection.nearestSampleValue(e, 230L)) // closer to 200 assertEquals(3f, InverseProjection.nearestSampleValue(e, 270L)) // closer to 300 } @Test fun nearestSampleValue_tieBreaksLower() { val e = newEntry() populate(e, listOf(100L to 1f, 200L to 2f)) // Midpoint 150: |100-150| == |200-150| == 50; idxA <= idxB → idxA = lo-1 = 0 → value 1f. assertEquals(1f, InverseProjection.nearestSampleValue(e, 150L)) } @Test fun nearestSampleValue_targetBeforeAllSamples() { val e = newEntry() populate(e, listOf(500L to 5f, 600L to 6f)) assertEquals(5f, InverseProjection.nearestSampleValue(e, 100L)) } @Test fun nearestSampleValue_targetAfterAllSamples() { val e = newEntry() populate(e, listOf(500L to 5f, 600L to 6f)) assertEquals(6f, InverseProjection.nearestSampleValue(e, 10_000L)) } @Test fun nearestSampleValue_emptyEntry_returnsNaN() { val e = newEntry() // scratchCount stays 0 val v = InverseProjection.nearestSampleValue(e, 100L) assertTrue(v.isNaN()) } @Test fun nearestSampleIndex_emptyReturnsNegOne() { val e = newEntry() assertEquals(-1, InverseProjection.nearestSampleIndex(e, 100L)) } @Test fun nearestSampleIndex_findsClosest() { val e = newEntry() populate(e, listOf(100L to 1f, 200L to 2f, 300L to 3f)) assertEquals(1, InverseProjection.nearestSampleIndex(e, 220L)) } }