package space.nstart.pcp.slots_service.service

import ballistics.types.InitialConditions
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyList
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.springframework.test.util.ReflectionTestUtils
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.requests.RequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.RequestWithCellsDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageBatchRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
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.pcp_types_lib.dto.satellite.SatelliteSlotProfileDTO
import space.nstart.pcp.slots_service.configuration.CustomValidationException
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 space.nstart.pcp.slots_service.model.satellite.TestSatelliteImpl
import space.nstart.pcp.slots_service.repository.BatchCoverageSearchRequest
import space.nstart.pcp.slots_service.repository.BatchCoverageSlotRow
import space.nstart.pcp.slots_service.repository.BookedRequestRepository
import space.nstart.pcp.slots_service.repository.BookedSlotsRepository
import space.nstart.pcp.slots_service.repository.SlotRepository
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import javax.sql.DataSource
import kotlin.test.assertEquals

class SlotServiceBatchTest {

    private fun <T> anyListValue(): List<T> = anyList<T>() ?: emptyList()
    private fun anyLongValue(): Long = anyLong()
    private fun anyIntValue(): Int = anyInt()

    @Test
    fun `polySlotsBatch uses single batch spatial fetch and keeps input target order`() {
        val slotRepository = mock(SlotRepository::class.java)
        val bookedSlotsRepository = mock(BookedSlotsRepository::class.java)

        val service = createService(
            slotRepository = slotRepository,
            bookedSlotsRepository = bookedSlotsRepository,
            catalogSatellites = listOf(defaultSatellite(22L))
        )

        `when`(
            slotRepository.findTargetSlotRows(
                anyListValue(),
                anyIntValue()
            )
        ).thenReturn(
            listOf(
                row(targetId = 2L, slotId = 1002L, satelliteId = 22L),
                row(targetId = 1L, slotId = 1001L, satelliteId = 22L)
            )
        )
        `when`(bookedSlotsRepository.findBySlot_SatelliteIdInAndCycleBetween(anyListValue(), anyLongValue(), anyLongValue())).thenReturn(emptyList())

        val response = service.polySlotsBatch(
            SlotCoverageBatchRequestDTO(
                items = listOf(
                    SlotCoverageTargetDTO(1, "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"),
                    SlotCoverageTargetDTO(2, "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")
                ),
                timeStart = java.time.LocalDateTime.of(2026, 3, 26, 10, 0),
                timeStop = java.time.LocalDateTime.of(2026, 3, 26, 12, 0),
                cov = false,
                satellites = listOf(22L)
            )
        )

        verify(bookedSlotsRepository, times(1))
            .findBySlot_SatelliteIdInAndCycleBetween(anyListValue(), anyLongValue(), anyLongValue())
        verify(slotRepository, times(1))
            .findTargetSlotRows(anyListValue(), anyIntValue())
        kotlin.test.assertEquals(2, response.size)
        kotlin.test.assertEquals(listOf(1L, 2L), response.map { it.targetId })
    }

    @Test
    fun `polySlotsBatch handles large spatial batch without secondary slot id fetch`() {
        val slotRepository = mock(SlotRepository::class.java)
        val bookedSlotsRepository = mock(BookedSlotsRepository::class.java)

        val service = createService(
            slotRepository = slotRepository,
            bookedSlotsRepository = bookedSlotsRepository,
            catalogSatellites = listOf(defaultSatellite(22L))
        )

        val rows = (1L..70_000L).map { slotId ->
            row(targetId = 1L, slotId = slotId, satelliteId = 22L)
        }

        `when`(
            slotRepository.findTargetSlotRows(
                anyListValue(),
                anyIntValue()
            )
        ).thenReturn(rows)
        `when`(bookedSlotsRepository.findBySlot_SatelliteIdInAndCycleBetween(anyListValue(), anyLongValue(), anyLongValue()))
            .thenReturn(emptyList())

        val response = service.polySlotsBatch(
            SlotCoverageBatchRequestDTO(
                items = listOf(SlotCoverageTargetDTO(1L, "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")),
                timeStart = LocalDateTime.of(2026, 3, 26, 10, 0),
                timeStop = LocalDateTime.of(2026, 3, 26, 12, 0),
                cov = false,
                satellites = listOf(22L)
            )
        )

        kotlin.test.assertEquals(1, response.size)
        verify(slotRepository, times(1))
            .findTargetSlotRows(anyListValue(), anyIntValue())
    }

    @Test
    fun `polySlotsBatch precomputes prepared slots once per satellite for whole batch`() {
        val slotRepository = mock(SlotRepository::class.java)
        val bookedSlotsRepository = mock(BookedSlotsRepository::class.java)

        val satellite = CountingSatellite(id = 22L)
        val service = createService(
            slotRepository = slotRepository,
            bookedSlotsRepository = bookedSlotsRepository,
            catalogSatellites = listOf(satellite)
        )

        `when`(
            slotRepository.findTargetSlotRows(
                anyListValue(),
                anyIntValue()
            )
        ).thenReturn(
            listOf(
                row(targetId = 1L, slotId = 1001L, satelliteId = 22L),
                row(targetId = 2L, slotId = 1001L, satelliteId = 22L)
            )
        )
        `when`(bookedSlotsRepository.findBySlot_SatelliteIdInAndCycleBetween(anyListValue(), anyLongValue(), anyLongValue()))
            .thenReturn(emptyList())

        val response = service.polySlotsBatch(
            SlotCoverageBatchRequestDTO(
                items = listOf(
                    SlotCoverageTargetDTO(1L, "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"),
                    SlotCoverageTargetDTO(2L, "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")
                ),
                timeStart = LocalDateTime.of(2026, 3, 26, 10, 0),
                timeStop = LocalDateTime.of(2026, 3, 26, 12, 0),
                cov = false,
                satellites = listOf(22L)
            )
        )

        assertEquals(1, satellite.prepareCoverageSlotsCalls)
        assertEquals(2, response.size)
        assertEquals(listOf(1, 1), response.map { it.slots.size })
    }

    @Test
    fun `polySlotsBatch pushes satellite and base time windows into spatial fetch request`() {
        val slotRepository = mock(SlotRepository::class.java)
        val bookedSlotsRepository = mock(BookedSlotsRepository::class.java)

        val satellite = defaultSatellite(
            id = 62138L,
            tnCalc = LocalDateTime.of(LocalDate.of(2025, 10, 27), LocalTime.of(0, 29, 23, 162630000)),
            cycleRevs = 243,
            durationCalc = 16,
            maxChainLength = 3
        )
        val service = createService(
            slotRepository = slotRepository,
            bookedSlotsRepository = bookedSlotsRepository,
            catalogSatellites = listOf(satellite)
        )
        val baseStart = satellite.tnCalc.plusHours(2)
        val requestTimeStart = baseStart.minusMinutes(1)
        val requestTimeStop = baseStart.plusMinutes(11)
        val capturedRequests = mutableListOf<BatchCoverageSearchRequest>()

        `when`(
            slotRepository.findTargetSlotRows(
                anyListValue(),
                anyIntValue()
            )
        ).thenAnswer { invocation ->
            capturedRequests += invocation.getArgument<List<BatchCoverageSearchRequest>>(0)
            listOf(row(targetId = 1L, slotId = 1001L, satelliteId = satellite.id))
        }
        `when`(bookedSlotsRepository.findBySlot_SatelliteIdInAndCycleBetween(anyListValue(), anyLongValue(), anyLongValue()))
            .thenReturn(emptyList())

        service.polySlotsBatch(
            SlotCoverageBatchRequestDTO(
                items = listOf(SlotCoverageTargetDTO(1L, "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")),
                timeStart = requestTimeStart,
                timeStop = requestTimeStop,
                cov = false,
                satellites = listOf(satellite.id)
            )
        )

        assertEquals(3, capturedRequests.size)
        assertEquals(setOf(satellite.id), capturedRequests.map { it.satelliteId }.toSet())
        assertEquals(setOf(1L), capturedRequests.map { it.targetId }.toSet())
        assertEquals(
            setOf(
                requestTimeStart to requestTimeStop
            ),
            capturedRequests.map { it.windowStart to it.windowStop }.toSet()
        )
    }

    @Test
    fun `polySlotsBatch applies continuous coverage strategy`() {
        val slotRepository = mock(SlotRepository::class.java)
        val bookedSlotsRepository = mock(BookedSlotsRepository::class.java)
        val service = createService(
            slotRepository = slotRepository,
            bookedSlotsRepository = bookedSlotsRepository,
            catalogSatellites = listOf(CountingSatellite(22L))
        )

        stubRowsForContinuousCoverage(slotRepository, bookedSlotsRepository)

        val response = service.polySlotsBatch(
            SlotCoverageBatchRequestDTO(
                items = listOf(SlotCoverageTargetDTO(1L, TARGET_WKT)),
                timeStart = LocalDateTime.of(2026, 3, 26, 10, 0),
                timeStop = LocalDateTime.of(2026, 3, 26, 12, 0),
                cov = true,
                satellites = listOf(22L),
                coverageStrategy = SlotCoverageStrategy.CONTINUOUS
            )
        )

        assertEquals(listOf(1L, 3L), response.single().slots.map { it.slotNumber })
    }

    @Test
    fun `polySlots applies continuous coverage strategy in single target path`() {
        val slotRepository = mock(SlotRepository::class.java)
        val bookedSlotsRepository = mock(BookedSlotsRepository::class.java)
        val service = createService(
            slotRepository = slotRepository,
            bookedSlotsRepository = bookedSlotsRepository,
            catalogSatellites = listOf(CountingSatellite(22L))
        )

        stubRowsForContinuousCoverage(slotRepository, bookedSlotsRepository)

        val response = service.polySlots(
            wkt = TARGET_WKT,
            timeStart = LocalDateTime.of(2026, 3, 26, 10, 0),
            timeStop = LocalDateTime.of(2026, 3, 26, 12, 0),
            sign = null,
            cov = true,
            sats = listOf(22L),
            coverageStrategy = SlotCoverageStrategy.CONTINUOUS
        ).toList()

        assertEquals(listOf(1L, 3L), response.map { it.slotNumber })
    }

    @Test
    fun `reqCover applies continuous coverage strategy`() {
        val slotRepository = mock(SlotRepository::class.java)
        val bookedSlotsRepository = mock(BookedSlotsRepository::class.java)
        val earthService = mock(EarthService::class.java)
        val service = createService(
            slotRepository = slotRepository,
            bookedSlotsRepository = bookedSlotsRepository,
            catalogSatellites = listOf(CountingSatellite(22L)),
            earthService = earthService
        )

        stubRowsForContinuousCoverage(slotRepository, bookedSlotsRepository)
        doReturn(RequestWithCellsDTO(request = RequestDTO(contour = TARGET_WKT)))
            .`when`(earthService).reqcells("req-1")

        val response = service.reqCover(
            id = "req-1",
            timeStart = LocalDateTime.of(2026, 3, 26, 10, 0),
            timeStop = LocalDateTime.of(2026, 3, 26, 12, 0),
            sign = null,
            cov = true,
            sats = listOf(22L),
            coverageStrategy = SlotCoverageStrategy.CONTINUOUS
        ).toList()

        assertEquals(listOf(1L, 3L), response.map { it.slotNumber })
    }

    private fun createService(
        slotRepository: SlotRepository,
        bookedSlotsRepository: BookedSlotsRepository,
        catalogSatellites: List<AbstractSatellite>,
        earthService: EarthService = mock(EarthService::class.java)
    ): SlotService {
        val service = SlotService()
        ReflectionTestUtils.setField(service, "slotRepository", slotRepository)
        ReflectionTestUtils.setField(service, "bookedSlotsRepository", bookedSlotsRepository)
        ReflectionTestUtils.setField(service, "requestRepository", mock(BookedRequestRepository::class.java))
        ReflectionTestUtils.setField(service, "earthService", earthService)
        ReflectionTestUtils.setField(service, "dataSource", mock(DataSource::class.java))
        ReflectionTestUtils.setField(service, "satelliteCatalogClient", testSatelliteCatalogClient(catalogSatellites))
        return service
    }

    private fun testSatelliteCatalogClient(satellites: List<AbstractSatellite>) = object : SatelliteCatalogClient {
        override fun allSatellites(): List<AbstractSatellite> = satellites

        override fun satellites(ids: List<Long>): List<AbstractSatellite> =
            ids.distinct().map { satelliteId ->
                satellites.find { it.id == satelliteId }
                    ?: throw CustomValidationException("КА $satelliteId не зарегистрирован")
            }

        override fun satellite(id: Long): AbstractSatellite =
            satellites.find { it.id == id }
                ?: throw CustomValidationException("КА $id не зарегистрирован")

        override fun updateSlotProfile(id: Long, request: SatelliteSlotProfileDTO): SatelliteSlotProfileDTO = request
    }

    private fun defaultSatellite(
        id: Long,
        tnCalc: LocalDateTime = LocalDateTime.of(2026, 3, 20, 7, 20),
        cycleRevs: Long = 275,
        durationCalc: Long = 18,
        maxChainLength: Int = 3
    ) = TestSatelliteImpl(
        id = id,
        cycleRevs = cycleRevs,
        tnCalc = tnCalc,
        durationCalc = durationCalc,
        maxChainLength = maxChainLength
    )

    private fun stubRowsForContinuousCoverage(
        slotRepository: SlotRepository,
        bookedSlotsRepository: BookedSlotsRepository
    ) {
        `when`(
            slotRepository.findTargetSlotRows(
                anyListValue(),
                anyIntValue()
            )
        ).thenReturn(
            listOf(
                row(
                    targetId = 1L,
                    slotId = 1L,
                    satelliteId = 22L,
                    contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
                    longitude = 0.0,
                    tn = LocalDateTime.of(2026, 3, 26, 10, 0, 0),
                    tk = LocalDateTime.of(2026, 3, 26, 10, 0, 10)
                ),
                row(
                    targetId = 1L,
                    slotId = 2L,
                    satelliteId = 22L,
                    contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))",
                    longitude = 0.0,
                    tn = LocalDateTime.of(2026, 3, 26, 10, 0, 20),
                    tk = LocalDateTime.of(2026, 3, 26, 10, 0, 30)
                ),
                row(
                    targetId = 1L,
                    slotId = 3L,
                    satelliteId = 22L,
                    contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))",
                    longitude = 1.0,
                    tn = LocalDateTime.of(2026, 3, 26, 10, 0, 40),
                    tk = LocalDateTime.of(2026, 3, 26, 10, 0, 50)
                )
            )
        )
        `when`(bookedSlotsRepository.findBySlot_SatelliteIdInAndCycleBetween(anyListValue(), anyLongValue(), anyLongValue()))
            .thenReturn(emptyList())
    }

    private fun row(
        targetId: Long,
        slotId: Long,
        satelliteId: Long,
        contour: String = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
        longitude: Double = 37.0,
        roll: Double = 10.0,
        revolutionSign: String = "ASC",
        tn: LocalDateTime = LocalDateTime.of(2026, 3, 26, 10, 0),
        tk: LocalDateTime = LocalDateTime.of(2026, 3, 26, 10, 10)
    ) = BatchCoverageSlotRow(
        targetId = targetId,
        slotId = slotId,
        slotNum = slotId,
        cycle = 0L,
        satelliteId = satelliteId,
        coveringType = 0,
        tn = tn,
        tk = tk,
        roll = roll,
        contour = contour,
        revolution = 1L,
        revolutionSign = revolutionSign,
        latitude = 55.0,
        longitude = longitude
    )

    private class CountingSatellite(id: Long) : AbstractSatellite(
        id = id,
        cycleRevs = 243,
        tnCalc = LocalDateTime.of(LocalDate.of(2025, 10, 27), LocalTime.of(0, 29, 23, 162630000)),
        durationCalc = 16,
        maxChainLength = 3,
        angles = emptyList(),
        ic = InitialConditions()
    ) {
        var prepareCoverageSlotsCalls: Int = 0

        override fun prepareCoverageSlots(
            slots: List<SlotEntity>,
            bookedSlots: List<BookedSlotEntity>,
            timeStart: LocalDateTime,
            timeStop: LocalDateTime,
            sign: RevolutionSign?,
            states: List<SlotStatus>?
        ): List<PreparedCoverageSlot> {
            prepareCoverageSlotsCalls++
            return slots.map { slot ->
                PreparedCoverageSlot(
                    sourceSlotId = slot.slotId ?: 0L,
                    slot = SlotDTO(
                        cycle = slot.cycle,
                        satelliteId = slot.satelliteId,
                        tn = slot.tn,
                        tk = slot.tk,
                        roll = slot.roll,
                        contour = slot.contour,
                        revolution = slot.revolution,
                        revolutionSign = RevolutionSign.valueOf(slot.revolutionSign),
                        slotNumber = slot.slotNum,
                        state = SlotStatus.AVAILABLE,
                        latitude = slot.latitude,
                        longitude = slot.longitude
                    )
                )
            }
        }
    }

    private companion object {
        private const val TARGET_WKT = "POLYGON ((0 0, 4 0, 4 1, 0 1, 0 0))"
    }
}
