package space.nstart.pcp.slots_service.service

import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.springframework.test.util.ReflectionTestUtils
import space.nstart.pcp.slots_service.entity.BookedRequestEntity
import space.nstart.pcp.slots_service.entity.BookedSlotEntity
import space.nstart.pcp.slots_service.entity.SlotEntity
import space.nstart.pcp.slots_service.model.satellite.TestSatelliteImpl
import space.nstart.pcp.slots_service.repository.BookedRequestRepository
import space.nstart.pcp.slots_service.repository.BookedSlotsRepository
import java.time.LocalDateTime
import kotlin.test.assertEquals

/**
 * Снятие брони request-aware. Ключевой инвариант: снятие НИКОГДА не удаляет BookedSlot,
 * шаренный между заявками, по id напрямую — рвёт только связь текущей заявки.
 */
class SlotServiceCancelTest {

    private val bookedSlotsRepository = mock(BookedSlotsRepository::class.java)
    private val requestRepository = mock(BookedRequestRepository::class.java)
    private val satelliteCatalogClient = mock(SatelliteCatalogClient::class.java)

    private fun service(): SlotService {
        val service = SlotService()
        ReflectionTestUtils.setField(service, "bookedSlotsRepository", bookedSlotsRepository)
        ReflectionTestUtils.setField(service, "requestRepository", requestRepository)
        ReflectionTestUtils.setField(service, "satelliteCatalogClient", satelliteCatalogClient)
        return service
    }

    @Test
    fun `cancelReqSlots deletes the slot when booked by a single request`() {
        val link = soleLink(bookedSlotId = 10L, linkId = 100L, requestId = "R1")
        `when`(requestRepository.findByBookedSlot_BookedSlotIdAndRequestId(10L, "R1")).thenReturn(link)

        val cnt = service().cancelReqSlots("R1", listOf(10L))

        assertEquals(1, cnt)
        verify(bookedSlotsRepository).deleteByBookedSlotId(10L)
        verify(requestRepository, never()).deleteByBookedSlotRequestId(org.mockito.ArgumentMatchers.anyLong())
    }

    @Test
    fun `cancelReqSlots only unlinks when slot is shared across requests`() {
        // Один и тот же слот забронирован под R1 и R2 — снятие для R1 не должно трогать сам слот.
        val link = sharedLink(bookedSlotId = 20L, linkId = 200L, otherLinkId = 201L, requestId = "R1")
        `when`(requestRepository.findByBookedSlot_BookedSlotIdAndRequestId(20L, "R1")).thenReturn(link)

        val cnt = service().cancelReqSlots("R1", listOf(20L))

        assertEquals(1, cnt)
        verify(requestRepository).deleteByBookedSlotRequestId(200L)
        verify(bookedSlotsRepository, never()).deleteByBookedSlotId(org.mockito.ArgumentMatchers.anyLong())
    }

    @Test
    fun `cancelReqSlots skips ids not booked by the request`() {
        `when`(requestRepository.findByBookedSlot_BookedSlotIdAndRequestId(99L, "R1")).thenReturn(null)
        val link = soleLink(bookedSlotId = 10L, linkId = 100L, requestId = "R1")
        `when`(requestRepository.findByBookedSlot_BookedSlotIdAndRequestId(10L, "R1")).thenReturn(link)

        val cnt = service().cancelReqSlots("R1", listOf(10L, 99L))

        assertEquals(1, cnt) // 99 пропущен
        verify(bookedSlotsRepository).deleteByBookedSlotId(10L)
    }

    @Test
    fun `cancelSlot delegates to request-aware single removal`() {
        val link = sharedLink(bookedSlotId = 20L, linkId = 200L, otherLinkId = 201L, requestId = "R1")
        `when`(requestRepository.findByBookedSlot_BookedSlotIdAndRequestId(20L, "R1")).thenReturn(link)

        val cnt = service().cancelSlot("R1", 20L)

        assertEquals(1, cnt)
        verify(requestRepository).deleteByBookedSlotRequestId(200L) // только связь, слот цел
        verify(bookedSlotsRepository, never()).deleteByBookedSlotId(org.mockito.ArgumentMatchers.anyLong())
    }

    @Test
    fun `bookedByReqId uses booking cycle and carries id plus contour`() {
        // slot.cycle (5) != booking cycle (3) — view ДОЛЖЕН вернуть 3, иначе ключ на фронте не сойдётся.
        val link = soleLink(
            bookedSlotId = 10L, linkId = 100L, requestId = "R1",
            bookingCycle = 3L, slotCycle = 5L, contour = "POLYGON ((0 0, 1 0, 1 1, 0 0))"
        )
        `when`(requestRepository.findByRequestId("R1")).thenReturn(listOf(link))

        val view = service().bookedByReqId("R1")

        assertEquals(1, view.size)
        assertEquals(10L, view[0].bookedSlotId)
        assertEquals(3L, view[0].cycle) // cycle БРОНИ, не slot.cycle (5)
        assertEquals("POLYGON ((0 0, 1 0, 1 1, 0 0))", view[0].contour)
    }

    @Test
    fun `bookedByReqId shifts tn-tk and revolution by booking cycle`() {
        // Регрессия на «слоты бронируются на не те даты»: by-request обязан отдавать слот, сдвинутый
        // на cycle брони (как request-cover), а не в базовом cycle-фрейме. cycle=2, durationCalc=16,
        // cycleRevs=243 -> сдвиг +32 суток и +486 витков относительно базового slot.tn/revolution.
        val link = soleLink(bookedSlotId = 10L, linkId = 100L, requestId = "R1", bookingCycle = 2L)
        `when`(requestRepository.findByRequestId("R1")).thenReturn(listOf(link))
        `when`(satelliteCatalogClient.satellite(77L))
            .thenReturn(TestSatelliteImpl(id = 77L, cycleRevs = 243L, durationCalc = 16L))

        val view = service().bookedByReqId("R1")

        assertEquals(1, view.size)
        assertEquals(2L, view[0].cycle)
        // базовый slot.tn/tk из builder slot(): 2026-03-31 10:00 / 10:10, базовый revolution = 0.
        assertEquals(LocalDateTime.of(2026, 3, 31, 10, 0).plusDays(32), view[0].tn)
        assertEquals(LocalDateTime.of(2026, 3, 31, 10, 10).plusDays(32), view[0].tk)
        assertEquals(486L, view[0].revolution)
    }

    @Test
    fun `bookedByReqId without satellite in catalog falls back to base frame`() {
        // КА нет в каталоге -> сдвиг невозможен, отдаём базовый фрейм без падения (лучше слот, чем 500).
        val link = soleLink(bookedSlotId = 10L, linkId = 100L, requestId = "R1", bookingCycle = 2L)
        `when`(requestRepository.findByRequestId("R1")).thenReturn(listOf(link))
        `when`(satelliteCatalogClient.satellite(77L)).thenThrow(RuntimeException("нет в каталоге"))

        val view = service().bookedByReqId("R1")

        assertEquals(1, view.size)
        assertEquals(LocalDateTime.of(2026, 3, 31, 10, 0), view[0].tn) // без сдвига
        assertEquals(2L, view[0].cycle) // cycle брони сохранён даже без КА
    }

    // --- builders ---

    private fun slot(
        slotId: Long, slotCycle: Long = 0L, contour: String = ""
    ) = SlotEntity(
        slotId = slotId,
        slotNum = slotId,
        cycle = slotCycle,
        satelliteId = 77L,
        contour = contour,
        tn = LocalDateTime.of(2026, 3, 31, 10, 0),
        tk = LocalDateTime.of(2026, 3, 31, 10, 10)
    )

    private fun soleLink(
        bookedSlotId: Long, linkId: Long, requestId: String,
        bookingCycle: Long = 0L, slotCycle: Long = 0L, contour: String = ""
    ): BookedRequestEntity {
        val booked = BookedSlotEntity(
            bookedSlotId = bookedSlotId,
            slot = slot(bookedSlotId, slotCycle, contour),
            cycle = bookingCycle,
            requests = mutableListOf()
        )
        val link = BookedRequestEntity(bookedSlotRequestId = linkId, bookedSlot = booked, requestId = requestId)
        booked.requests!!.add(link)
        return link
    }

    private fun sharedLink(
        bookedSlotId: Long, linkId: Long, otherLinkId: Long, requestId: String
    ): BookedRequestEntity {
        val booked = BookedSlotEntity(
            bookedSlotId = bookedSlotId,
            slot = slot(bookedSlotId),
            cycle = 0L,
            requests = mutableListOf()
        )
        val link = BookedRequestEntity(bookedSlotRequestId = linkId, bookedSlot = booked, requestId = requestId)
        val other = BookedRequestEntity(bookedSlotRequestId = otherLinkId, bookedSlot = booked, requestId = "R2")
        booked.requests!!.add(link)
        booked.requests!!.add(other)
        return link
    }
}
