package space.nstart.pcp_tgu_service.service

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import space.nstart.pcp_tgu_service.config.PlanningProperties
import space.nstart.pcp_tgu_service.domain.InsertionPoint
import space.nstart.pcp_tgu_service.domain.ObservationWindow
import space.nstart.pcp_tgu_service.domain.SpacecraftPoints
import java.time.LocalDateTime

class PlanCalculationServiceTest {

    @Test
    fun `calculatePlans keeps neighbouring plans overlapped by one window when possible`() {
        val service = createService(targetMinutes = 24 * 60L, deltaMinutes = 20L)
        val start = LocalDateTime.of(2026, 3, 10, 10, 0)

        val plans = service.calculatePlans(
            spacecraftPoints(
                window("KPP-1", start),
                window("KPP-2", start.plusHours(8)),
                window("KPP-3", start.plusHours(16)),
                window("KPP-4", start.plusHours(23).plusMinutes(55), 15),
                window("KPP-5", start.plusHours(24).plusMinutes(10)),
                window("KPP-6", start.plusHours(32)),
                window("KPP-7", start.plusHours(40)),
                window("KPP-8", start.plusHours(47).plusMinutes(55), 15)
            )
        )

        assertEquals(4, plans.size)
        assertEquals(start, plans[0].startTime)
        assertEquals(start.plusDays(1).plusMinutes(10), plans[0].endTime)
        assertEquals(start.plusHours(16), plans[1].startTime)
        assertEquals(start.plusDays(1).plusHours(16).plusMinutes(10), plans[1].endTime)
        assertEquals("KPP-3", plans[1].kppId)
        assertEquals(start.plusHours(32), plans[2].startTime)
        assertEquals(start.plusDays(2).plusMinutes(10), plans[2].endTime)
        assertEquals(start.plusHours(40), plans[3].startTime)
        assertEquals(start.plusDays(2).plusMinutes(10), plans[3].endTime)
    }

    @Test
    fun `calculatePlansAfter keeps one window overlap with previous plan when it can be matched`() {
        val service = createService(targetMinutes = 24 * 60L, deltaMinutes = 20L)
        val start = LocalDateTime.of(2026, 3, 10, 10, 0)
        val spacecraftPoints = spacecraftPoints(
            window("KPP-1", start),
            window("KPP-2", start.plusHours(8)),
            window("KPP-3", start.plusHours(16)),
            window("KPP-4", start.plusHours(23).plusMinutes(55), 15),
            window("KPP-5", start.plusHours(24).plusMinutes(10)),
            window("KPP-6", start.plusHours(32)),
            window("KPP-7", start.plusHours(40)),
            window("KPP-8", start.plusHours(47).plusMinutes(55), 15)
        )
        val fullPlans = service.calculatePlans(spacecraftPoints)

        val plansAfterFirst = service.calculatePlansAfter(spacecraftPoints, fullPlans.first())

        assertEquals(3, plansAfterFirst.size)
        assertEquals(start.plusHours(16), plansAfterFirst[0].startTime)
        assertEquals(start.plusDays(1).plusHours(16).plusMinutes(10), plansAfterFirst[0].endTime)
        assertEquals("KPP-3", plansAfterFirst[0].kppId)
        assertEquals(start.plusHours(32), plansAfterFirst[1].startTime)
        assertEquals(start.plusDays(2).plusMinutes(10), plansAfterFirst[1].endTime)
        assertEquals(start.plusHours(40), plansAfterFirst[2].startTime)
        assertEquals(start.plusDays(2).plusMinutes(10), plansAfterFirst[2].endTime)
    }

    @Test
    fun `calculatePlans returns empty list when only one window is available`() {
        val service = createService(targetMinutes = 24 * 60L, deltaMinutes = 60L)
        val start = LocalDateTime.of(2026, 3, 10, 10, 0)

        val plans = service.calculatePlans(
            spacecraftPoints(
                window("KPP-1", start)
            )
        )

        assertTrue(plans.isEmpty())
    }

    @Test
    fun `calculatePlans falls back to the closest end point when tail is outside delta`() {
        val service = createService(targetMinutes = 24 * 60L, deltaMinutes = 20L)
        val start = LocalDateTime.of(2026, 3, 10, 10, 0)

        val plans = service.calculatePlans(
            spacecraftPoints(
                window("KPP-1", start),
                window("KPP-2", start.plusHours(23).plusMinutes(55), 15),
                window("KPP-3", start.plusHours(30))
            )
        )

        assertEquals(2, plans.size)
        assertEquals(start, plans[0].startTime)
        assertEquals(start.plusDays(1).plusMinutes(10), plans[0].endTime)
        assertEquals(start.plusHours(23).plusMinutes(55), plans[1].startTime)
        assertEquals(start.plusHours(30).plusMinutes(10), plans[1].endTime)
    }

    @Test
    fun `calculateNearestPlanFrom uses nearest start and returns closest available end even outside delta`() {
        val service = createService(targetMinutes = 24 * 60L, deltaMinutes = 20L)
        val start = LocalDateTime.of(2026, 3, 10, 10, 0)

        val plan = service.calculateNearestPlanFrom(
            spacecraftPoints(
                window("KPP-NEAREST", start),
                window("KPP-SHORT-END", start.plusHours(2)),
                window("KPP-LONG-END", start.plusDays(2))
            ),
            fromTime = start
        )

        assertEquals(start, plan!!.startTime)
        assertEquals(start.plusHours(2).plusMinutes(10), plan.endTime)
        assertEquals("KPP-NEAREST", plan.kppId)
    }

    @Test
    fun `calculatePlans moves forward when overlap is impossible`() {
        val service = createService(targetMinutes = 60L, deltaMinutes = 10L)
        val start = LocalDateTime.of(2026, 3, 10, 10, 0)

        val plans = service.calculatePlans(
            spacecraftPoints(
                window("KPP-1", start),
                window("KPP-2", start.plusMinutes(55), 15),
                window("KPP-3", start.plusHours(1).plusMinutes(30), 35)
            )
        )

        assertEquals(2, plans.size)
        assertEquals(start, plans[0].startTime)
        assertEquals(start.plusMinutes(55), plans[1].startTime)
    }

    @Test
    fun `calculatePlans picks duration closest to target when windows are separated by large gaps`() {
        val service = createService(targetMinutes = 24 * 60L, deltaMinutes = 20L)
        val start = LocalDateTime.of(2026, 3, 10, 10, 0)

        val plans = service.calculatePlans(
            spacecraftPoints(
                window("KPP-1", start),
                window("KPP-2", start.plusHours(1).plusMinutes(30)),
                window("KPP-3", start.plusDays(2))
            )
        )

        assertEquals(2, plans.size)
        assertEquals(start, plans[0].startTime)
        assertEquals(start.plusHours(1).plusMinutes(40), plans[0].endTime)
        assertEquals(start.plusHours(1).plusMinutes(30), plans[1].startTime)
        assertEquals(start.plusDays(2).plusMinutes(10), plans[1].endTime)
    }


    @Test
    fun `calculatePlans chooses plan closest to target even when only short and long options exist`() {
        val service = createService(targetMinutes = 24 * 60L, deltaMinutes = 20L)
        val start = LocalDateTime.of(2026, 3, 10, 10, 0)

        val plans = service.calculatePlans(
            spacecraftPoints(
                window("KPP-1", start),
                window("KPP-2", start.plusHours(1).plusMinutes(30)),
                window("KPP-3", start.plusDays(2))
            )
        )

        assertEquals(start.plusHours(1).plusMinutes(40), plans.first().endTime)
    }

    @Test
    fun `calculatePlans handles ten simultaneous windows without looping`() {
        val service = createService(targetMinutes = 24 * 60L, deltaMinutes = 20L)
        val start = LocalDateTime.of(2026, 3, 10, 10, 0)
        val simultaneousWindows = buildList {
            for (index in 1..10) {
                add(window("SIM-$index", start))
            }
            for (index in 1..10) {
                add(window("END-$index", start.plusHours(23).plusMinutes(55), 15))
            }
        }

        val plans = service.calculatePlans(spacecraftPoints(*simultaneousWindows.toTypedArray()))

        assertTrue(plans.isNotEmpty())
        assertTrue(plans.size <= simultaneousWindows.size)
        assertEquals(start, plans.first().startTime)
        assertEquals(start.plusDays(1).plusMinutes(10), plans.first().endTime)
    }

    private fun createService(targetMinutes: Long, deltaMinutes: Long): PlanCalculationService =
        PlanCalculationService(
            PlanningProperties(
                notificationBeforeStartMinutes = 30,
                targetPlanDurationMinutes = targetMinutes,
                targetPlanDurationDeltaMinutes = deltaMinutes,
                externalUpdateEnabled = false,
                externalPointsLookaheadDays = 3,
                inMemoryHistoryDays = 14,
                externalUpdateFixedDelayMs = 0,
                notificationCheckFixedDelayMs = 30_000,
                minReissueLeadMinutes = 5,
                workerLockTtlSeconds = 300
            )
        )

    private fun spacecraftPoints(vararg windows: Pair<String, ObservationWindow>): SpacecraftPoints =
        SpacecraftPoints(
            spacecraftId = "25544",
            points = windows
                .groupBy({ it.first }, { it.second })
                .map { (pointId, observationWindows) ->
                    InsertionPoint(
                        pointId = pointId,
                        observationWindows = observationWindows
                    )
                }
        )

    private fun window(pointId: String, startTime: LocalDateTime, durationMinutes: Long = 10): Pair<String, ObservationWindow> =
        pointId to ObservationWindow(
            startTime = startTime,
            endTime = startTime.plusMinutes(durationMinutes),
            elevationAngle = 10.0
        )
}
