package space.nstart.pcp.slots_service.service

import ballistics.orbitalPoints.timeStepper.AbstractStepper
import ballistics.types.OrbitalPoint
import ballistics.utils.math.Vector3D
import org.junit.jupiter.api.Test
import org.springframework.test.util.ReflectionTestUtils
import space.nstart.pcp.slots_service.model.Mar
import java.time.LocalDateTime
import kotlin.math.cos
import kotlin.math.sin
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class SlotServiceCalcSlotsTest {

    @Test
    fun `buildSlotCalculationChunks creates aligned half-open windows without gaps`() {
        val service = SlotService()
        ReflectionTestUtils.setField(service, "slotCalcChunkDurationSeconds", 20L)
        ReflectionTestUtils.setField(service, "slotCalcSourceMarginSeconds", 120L)

        val start = LocalDateTime.of(2026, 4, 9, 10, 0, 0)
        val endExclusive = start.plusSeconds(60)

        val chunks = ReflectionTestUtils.invokeMethod<List<Any>>(
            service,
            "buildSlotCalculationChunks",
            start,
            endExclusive,
            10L,
            60L
        ).orEmpty()

        assertEquals(3, chunks.size)
        assertEquals(start, ReflectionTestUtils.getField(chunks[0], "generationStart"))
        assertEquals(start.plusSeconds(20), ReflectionTestUtils.getField(chunks[0], "generationEndExclusive"))
        assertEquals(start.plusSeconds(20), ReflectionTestUtils.getField(chunks[1], "generationStart"))
        assertEquals(start.plusSeconds(40), ReflectionTestUtils.getField(chunks[1], "generationEndExclusive"))
        assertEquals(start.plusSeconds(40), ReflectionTestUtils.getField(chunks[2], "generationStart"))
        assertEquals(endExclusive, ReflectionTestUtils.getField(chunks[2], "generationEndExclusive"))
        assertEquals(5L, ReflectionTestUtils.getField(chunks[0], "generationStepSeconds"))

        val sourceWindowStartSeconds = ReflectionTestUtils.getField(chunks[0], "sourceWindowStartSeconds") as Double
        val sourceWindowEndSeconds = ReflectionTestUtils.getField(chunks[0], "sourceWindowEndSeconds") as Double
        assertTrue(sourceWindowStartSeconds < ballistics.utils.fromDateTime(start))
        assertTrue(sourceWindowEndSeconds > ballistics.utils.fromDateTime(start.plusSeconds(30)))
    }

    @Test
    fun `drainReadyChunkResults preserves deterministic chunk order`() {
        val service = SlotService()
        val start = LocalDateTime.of(2026, 4, 9, 10, 0, 0)
        val pending = linkedMapOf(
            2 to pendingChunk(2, start.plusSeconds(40), start.plusSeconds(60)),
            0 to pendingChunk(0, start, start.plusSeconds(20)),
            1 to pendingChunk(1, start.plusSeconds(20), start.plusSeconds(40))
        )

        val drained = ReflectionTestUtils.invokeMethod<Pair<List<Any>, Int>>(
            service,
            "drainReadyChunkResults",
            pending,
            0
        )!!

        assertEquals(
            listOf(0, 1, 2),
            drained.first.map { ReflectionTestUtils.getField(ReflectionTestUtils.getField(it, "completion")!!, "index") }
        )
        assertEquals(3, drained.second)
        assertTrue(pending.isEmpty())
    }

    @Test
    fun `prepared orbital window is reused across multiple contours`() {
        val service = SlotService()
        val stepper = CountingStepper()
        val start = LocalDateTime.of(2026, 4, 9, 10, 0, 0)
        val endInclusive = start.plusSeconds(10)

        val window = prepareOrbitalWindow(service, stepper, start, endInclusive)

        val sampledPoints = ReflectionTestUtils.getField(window, "points") as List<*>
        assertEquals(3, sampledPoints.size)
        assertEquals(3, stepper.requestedTimes.size)

        val firstContour = contourFromPreparedWindow(service, window, 20.0, 2.0)
        val secondContour = contourFromPreparedWindow(service, window, 24.0, 2.0)

        assertTrue(firstContour.isNotBlank())
        assertTrue(secondContour.isNotBlank())
        assertEquals(3, stepper.requestedTimes.size)
        assertEquals(
            listOf(5.0, 5.0),
            stepper.requestedTimes.zipWithNext { left, right -> right - left }
        )
    }

    @Test
    fun `prepareOrbitalWindow reuses initial tn point when provided`() {
        val service = SlotService()
        val stepper = CountingStepper()
        val start = LocalDateTime.of(2026, 4, 9, 10, 0, 0)
        val endInclusive = start.plusSeconds(10)

        val initialPoint = stepper.calculate(ballistics.utils.fromDateTime(start))
        val callsAfterInitialPoint = stepper.requestedTimes.size

        val window = prepareOrbitalWindow(service, stepper, start, endInclusive, initialPoint)

        val sampledPoints = ReflectionTestUtils.getField(window, "points") as List<*>
        assertEquals(3, sampledPoints.size)
        assertEquals(callsAfterInitialPoint + 2, stepper.requestedTimes.size)
    }

    private fun pendingChunk(
        index: Int,
        start: LocalDateTime,
        endExclusive: LocalDateTime
    ): Any {
        val completionClass = Class.forName(
            "space.nstart.pcp.slots_service.service.SlotService\$SlotCalculationChunkComplete"
        )
        val completionConstructor = completionClass.declaredConstructors.single { it.parameterCount == 11 }
        completionConstructor.isAccessible = true
        val completion = completionConstructor.newInstance(
            index,
            10L,
            start,
            endExclusive,
            1,
            1,
            5L,
            2L,
            3L,
            1,
            2
        )

        val pendingChunkClass = Class.forName(
            "space.nstart.pcp.slots_service.service.SlotService\$SlotCalculationPendingChunk"
        )
        val pendingChunkConstructor = pendingChunkClass.declaredConstructors.single { it.parameterCount == 2 }
        pendingChunkConstructor.isAccessible = true
        return pendingChunkConstructor.newInstance(
            mutableListOf(listOf(Mar(sat = 1L, tn = start, tk = endExclusive))),
            completion
        )
    }

    private fun prepareOrbitalWindow(
        service: SlotService,
        stepper: CountingStepper,
        start: LocalDateTime,
        endInclusive: LocalDateTime,
        initialPoint: OrbitalPoint? = null
    ): Any {
        val method = SlotService::class.java.getDeclaredMethod(
            "prepareOrbitalWindow",
            AbstractStepper::class.java,
            LocalDateTime::class.java,
            LocalDateTime::class.java,
            OrbitalPoint::class.java
        )
        method.isAccessible = true
        return method.invoke(service, stepper, start, endInclusive, initialPoint)!!
    }

    private fun contourFromPreparedWindow(
        service: SlotService,
        window: Any,
        roll: Double,
        capture: Double
    ): String {
        val preparedWindowClass = Class.forName(
            "space.nstart.pcp.slots_service.service.SlotService\$PreparedOrbitalWindow"
        )
        val method = SlotService::class.java.getDeclaredMethod(
            "contour",
            preparedWindowClass,
            java.lang.Double.TYPE,
            java.lang.Double.TYPE
        )
        method.isAccessible = true
        return method.invoke(service, window, roll, capture) as String
    }

    private class CountingStepper : AbstractStepper {
        val requestedTimes = mutableListOf<Double>()

        override fun calculate(t: Double): OrbitalPoint {
            requestedTimes += t
            val phase = t / 600.0
            return OrbitalPoint(
                t,
                42,
                Vector3D(7_000_000.0 * cos(phase), 7_000_000.0 * sin(phase), 1_000_000.0),
                Vector3D(-7_500.0 * sin(phase), 7_500.0 * cos(phase), 10.0)
            )
        }

        override fun calculate(t: Double, prev: OrbitalPoint): OrbitalPoint = calculate(t)

        override fun clear() = Unit
    }
}
