package space.nstart.pcp.slots_service.service

import org.junit.jupiter.api.Test
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotStatus
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
import space.nstart.pcp.slots_service.entity.SlotEntity
import space.nstart.pcp.slots_service.model.satellite.AbstractSatellite
import space.nstart.pcp.slots_service.model.satellite.PreparedCoverageSlot
import java.time.LocalDateTime
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class OccupiedIntervalPrefilterTest {

    @Test
    fun `different roll blocks candidate inside occupied mmi window`() {
        val satellite = testSatellite(maxSurveyDuration = 120, mmi = 5)
        val candidate = candidateSlot(roll = 12.0, startOffsetSeconds = 44, endOffsetSeconds = 70)
        val occupied = listOf(occupiedInterval(roll = 10.0, startOffsetSeconds = 0, endOffsetSeconds = 40))

        val filtered = OccupiedIntervalPrefilter.filterCandidates(listOf(candidate), occupied, satellite)

        assertTrue(filtered.isEmpty())
    }

    @Test
    fun `same roll continuation is allowed when merged duration fits duration max`() {
        val satellite = testSatellite(maxSurveyDuration = 120, mmi = 5)
        val candidate = candidateSlot(roll = 10.0, startOffsetSeconds = 44, endOffsetSeconds = 70)
        val occupied = listOf(occupiedInterval(roll = 10.0, startOffsetSeconds = 0, endOffsetSeconds = 40))

        val filtered = OccupiedIntervalPrefilter.filterCandidates(listOf(candidate), occupied, satellite)

        assertEquals(listOf(candidate), filtered)
    }

    @Test
    fun `same roll continuation is rejected when merged duration exceeds duration max`() {
        val satellite = testSatellite(maxSurveyDuration = 60, mmi = 5)
        val candidate = candidateSlot(roll = 10.0, startOffsetSeconds = 44, endOffsetSeconds = 70)
        val occupied = listOf(occupiedInterval(roll = 10.0, startOffsetSeconds = 0, endOffsetSeconds = 40))

        val filtered = OccupiedIntervalPrefilter.filterCandidates(listOf(candidate), occupied, satellite)

        assertTrue(filtered.isEmpty())
    }

    @Test
    fun `occupied intervals with same roll normalize into one chain`() {
        val satellite = testSatellite(maxSurveyDuration = 120, mmi = 5)
        val normalized = OccupiedIntervalPrefilter.normalize(
            occupiedIntervals = listOf(
                occupiedInterval(roll = 10.0, startOffsetSeconds = 0, endOffsetSeconds = 40),
                occupiedInterval(roll = 10.0, startOffsetSeconds = 43, endOffsetSeconds = 60)
            ),
            satellite = satellite
        )

        assertEquals(1, normalized.size)
        assertEquals(baseTime().plusSeconds(0), normalized.single().startTime)
        assertEquals(baseTime().plusSeconds(60), normalized.single().endTime)
    }

    @Test
    fun `candidate can bridge two same-roll occupied chains within duration max`() {
        val satellite = testSatellite(maxSurveyDuration = 90, mmi = 5)
        val candidate = candidateSlot(roll = 10.0, startOffsetSeconds = 24, endOffsetSeconds = 46)
        val occupied = OccupiedIntervalPrefilter.normalize(
            occupiedIntervals = listOf(
                occupiedInterval(roll = 10.0, startOffsetSeconds = 0, endOffsetSeconds = 20),
                occupiedInterval(roll = 10.0, startOffsetSeconds = 50, endOffsetSeconds = 70)
            ),
            satellite = satellite
        )

        val filtered = OccupiedIntervalPrefilter.filterCandidates(listOf(candidate), occupied, satellite)

        assertEquals(listOf(candidate), filtered)
    }


    @Test
    fun `available candidate inside reserved slots interval is rejected even with same roll`() {
        val satellite = testSatellite(maxSurveyDuration = 120, mmi = 5)
        val candidate = candidateSlot(roll = 10.0, startOffsetSeconds = 10, endOffsetSeconds = 20)
        val occupied = listOf(
            occupiedInterval(
                roll = 10.0,
                startOffsetSeconds = 0,
                endOffsetSeconds = 30,
                source = "SLOTS"
            )
        )

        val filtered = OccupiedIntervalPrefilter.filterCandidates(listOf(candidate), occupied, satellite)

        assertTrue(filtered.isEmpty())
    }

    @Test
    fun `booked candidate inside reserved slots interval is kept for coverage accounting`() {
        val satellite = testSatellite(maxSurveyDuration = 120, mmi = 5)
        val candidate = candidateSlot(
            roll = 10.0,
            startOffsetSeconds = 10,
            endOffsetSeconds = 20,
            state = SlotStatus.BOOKED
        )
        val occupied = listOf(
            occupiedInterval(
                roll = 10.0,
                startOffsetSeconds = 0,
                endOffsetSeconds = 30,
                source = "SLOTS"
            )
        )

        val filtered = OccupiedIntervalPrefilter.filterCandidates(listOf(candidate), occupied, satellite)

        assertEquals(listOf(candidate), filtered)
    }

    private fun testSatellite(maxSurveyDuration: Int, mmi: Long): AbstractSatellite =
        object : AbstractSatellite(id = 22, maxSurveyDuration = maxSurveyDuration, mmi = mmi) {
            override fun prepareCoverageSlots(
                slots: List<SlotEntity>,
                bookedSlots: List<BookedSlotEntity>,
                timeStart: LocalDateTime,
                timeStop: LocalDateTime,
                sign: RevolutionSign?,
                states: List<SlotStatus>?
            ): List<PreparedCoverageSlot> = emptyList()
        }

    private fun occupiedInterval(
        roll: Double,
        startOffsetSeconds: Long,
        endOffsetSeconds: Long,
        source: String = "TEST"
    ) = OccupiedIntervalDTO(
        satelliteId = 22,
        startTime = baseTime().plusSeconds(startOffsetSeconds),
        endTime = baseTime().plusSeconds(endOffsetSeconds),
        roll = roll,
        source = source
    )

    private fun candidateSlot(
        roll: Double,
        startOffsetSeconds: Long,
        endOffsetSeconds: Long,
        state: SlotStatus = SlotStatus.AVAILABLE
    ) = SlotDTO(
        cycle = 0,
        satelliteId = 22,
        tn = baseTime().plusSeconds(startOffsetSeconds),
        tk = baseTime().plusSeconds(endOffsetSeconds),
        roll = roll,
        contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
        state = state
    )

    private fun baseTime(): LocalDateTime = LocalDateTime.of(2026, 3, 26, 10, 0, 0)
}
