package solutions.tretter.mindmachine.session

import androidx.compose.ui.geometry.Offset
import org.junit.Assert.*
import org.junit.Test

/**
 * Unit tests for TimelineCurve gesture handling logic.
 * These tests verify single-finger point dragging and hit testing without requiring a device or emulator.
 */
class TimelineCurveGestureTest {

    @Test
    fun `movePointVertical updates value correctly`() {
        val curve = TimelineCurve.constant(0.5f, 600)
        val heightPx = 200f

        // Move point at index 0 (time=0) to Y=0 (should be value=1.0)
        val result = curve.movePointVertical(0, 0f, heightPx)

        assertEquals(1.0f, result.points[0].value01, 0.01f)

        // Move to bottom of screen (Y=heightPx) should be value=0.0
        val result2 = curve.movePointVertical(0, heightPx, heightPx)
        assertEquals(0.0f, result2.points[0].value01, 0.01f)

        // Move to middle should be value=0.5
        val result3 = curve.movePointVertical(0, heightPx / 2, heightPx)
        assertEquals(0.5f, result3.points[0].value01, 0.01f)
    }

    @Test
    fun `movePointVertical respects value bounds 0 to 1`() {
        val curve = TimelineCurve.constant(0.5f, 600)
        val heightPx = 200f

        // Try to move above top (negative Y)
        val result = curve.movePointVertical(0, -50f, heightPx)
        assertTrue(result.points[0].value01 >= 0f)
        assertTrue(result.points[0].value01 <= 1f)

        // Try to move below bottom (Y > height)
        val result2 = curve.movePointVertical(0, 250f, heightPx)
        assertTrue(result2.points[0].value01 >= 0f)
        assertTrue(result2.points[0].value01 <= 1f)
    }

    @Test
    fun `movePointVertical returns same curve for invalid index`() {
        val curve = TimelineCurve.constant(0.5f, 600)
        val heightPx = 200f

        // Try to move point at invalid index
        val result = curve.movePointVertical(999, 0f, heightPx)

        // Should return the original curve unchanged
        assertEquals(curve.points.size, result.points.size)
        assertEquals(curve.points[0].value01, result.points[0].value01, 0.01f)
    }

    @Test
    fun `hitTestPoint finds nearest point within radius`() {
        val curve = TimelineCurve.constant(0.5f, 600)
        val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
        val widthPx = 400f
        val heightPx = 200f

        // Point at index 0 is at time=0, value=0.5
        // timeToX(0) = 0, valueToY(0.5) = 100 (halfway up)
        val offset = Offset(x = 10f, y = 100f) // Close to point at index 0

        val hitIndex = curve.hitTestPoint(offset, viewport, widthPx, heightPx)

        // Should hit the first point (index 0)
        assertNotNull(hitIndex)
        assertEquals(0, hitIndex)
    }

    @Test
    fun `hitTestPoint returns null when outside radius`() {
        val curve = TimelineCurve.constant(0.5f, 600)
        val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
        val widthPx = 400f
        val heightPx = 200f

        // Point far away from any curve point
        val offset = Offset(x = 500f, y = 0f)

        val hitIndex = curve.hitTestPoint(offset, viewport, widthPx, heightPx)

        assertNull(hitIndex)
    }

    @Test
    fun `hitTestPoint finds closest point when multiple within radius`() {
        // Create a curve with points close together
        val curve = TimelineCurve.constant(0.5f, 120) // Points every minute (60 sec)
        val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 120f)
        val widthPx = 400f
        val heightPx = 200f

        // Point near middle of screen (around index 1-2)
        // x=200 should be at time=60 seconds (index 1)
        val offset = Offset(x = 200f, y = 100f)

        val hitIndex = curve.hitTestPoint(offset, viewport, widthPx, heightPx)

        assertNotNull(hitIndex)
        // Should hit a point near the middle (not too far away)
        assertTrue(hitIndex!! < 10)
    }

    @Test
    fun `hitTestPoint filters out points outside viewport bounds`() {
        val curve = TimelineCurve.constant(0.5f, 600)
        // Viewport only shows first 60 seconds
        val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 60f)
        val widthPx = 400f
        val heightPx = 200f

        // Try to hit a point at time=300 seconds (far outside viewport)
        // This should be clipped and not hit
        val offset = Offset(x = 3000f, y = 100f) // Way off screen

        val hitIndex = curve.hitTestPoint(offset, viewport, widthPx, heightPx)

        assertNull(hitIndex)
    }

    @Test
    fun `valueAt returns interpolated values between points`() {
        val points = listOf(
            TimelinePoint(0, 0.0f),   // At time 0, value is 0
            TimelinePoint(100, 1.0f), // At time 100, value is 1
        )
        val curve = TimelineCurve(points)

        // Midpoint should interpolate to 0.5
        val midValue = curve.valueAt(50f)
        assertEquals(0.5f, midValue, 0.01f)

        // Quarter point should interpolate to 0.25
        val quarterValue = curve.valueAt(25f)
        assertEquals(0.25f, quarterValue, 0.01f)
    }

    @Test
    fun `valueAt clamps to first point before start`() {
        val points = listOf(
            TimelinePoint(100, 0.5f),
            TimelinePoint(200, 0.8f),
        )
        val curve = TimelineCurve(points)

        // Before first point should return first point's value
        val beforeValue = curve.valueAt(50f)
        assertEquals(0.5f, beforeValue, 0.01f)
    }

    @Test
    fun `valueAt clamps to last point after end`() {
        val points = listOf(
            TimelinePoint(100, 0.5f),
            TimelinePoint(200, 0.8f),
        )
        val curve = TimelineCurve(points)

        // After last point should return last point's value
        val afterValue = curve.valueAt(250f)
        assertEquals(0.8f, afterValue, 0.01f)
    }

    @Test
    fun `ensureTimelinePoints snaps points to selected boundaries`() {
        val points = listOf(
            TimelinePoint(0, 0.0f),
            TimelinePoint(30, 0.5f), // 30 seconds - not a minute boundary
            TimelinePoint(120, 0.8f),
        )
        val curve = TimelineCurve(points)

        val result = curve.ensureTimelinePoints(180, granularitySec = 30)

        // Check that all points are on selected boundaries (multiples of 30)
        result.points.forEach { point ->
            assertTrue("Point at ${point.tSec} should be on 30s boundary", point.tSec % 30 == 0)
        }

        // Should have points at 0, 30, 60, 90, 120, 150, 180 seconds
        assertEquals(7, result.points.size)
    }

    @Test
    fun `ensureTimelinePoints preserves interpolated values`() {
        val points = listOf(
            TimelinePoint(0, 0.0f),
            TimelinePoint(120, 1.0f),
        )
        val curve = TimelineCurve(points)

        val result = curve.ensureTimelinePoints(180, granularitySec = 60)

        // Value at 60 seconds should interpolate to 0.5
        val valueAt60 = result.valueAt(60f)
        assertEquals(0.5f, valueAt60, 0.01f)
    }

    @Test
    fun `extendTo adds points for extended duration`() {
        val curve = TimelineCurve.constant(0.5f, 60)

        val extended = curve.extendTo(120, granularitySec = 60)

        // Should have points covering up to 120 seconds
        assertTrue(extended.points.any { it.tSec == 120 })
        // Points should be at minute intervals
        assertTrue(extended.points.size > 2)
    }
}
