package space.nstart.pcp.slots_service.service

import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.springframework.test.util.ReflectionTestUtils
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
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.SlotEntity
import space.nstart.pcp.slots_service.model.satellite.AbstractSatellite
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 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
import kotlin.test.assertFailsWith

class SlotServiceIntervalTest {

    private fun anyDateTimeValue(): LocalDateTime = any(LocalDateTime::class.java) ?: LocalDateTime.MIN
    private fun eqLongValue(value: Long): Long = eq(value) ?: value
    private fun eqDateTimeValue(value: LocalDateTime): LocalDateTime = eq(value) ?: value

    private data class TestSlotCalculationSummaryProjection(
        override val satelliteId: Long,
        override val slotCount: Long,
        override val minTime: LocalDateTime?,
        override val maxTime: LocalDateTime?,
        override val minRevolution: Long?,
        override val maxRevolution: Long?,
    ) : SlotRepository.SlotCalculationSummaryProjection

    @Test
    fun `allByInterval shifts base slots by closure cycle`() {
        val slotRepository = mock(SlotRepository::class.java)
        val satellite = TestSatelliteImpl(
            id = 22L,
            cycleRevs = 243L,
            tnCalc = LocalDateTime.of(LocalDate.of(2026, 3, 31), LocalTime.of(0, 0)),
            durationCalc = 16
        )
        val service = slotServiceFixture(slotRepository, satellite)

        val intervalStart = LocalDateTime.of(2026, 4, 16, 9, 59)
        val intervalStop = LocalDateTime.of(2026, 4, 16, 10, 11)
        val baseWindowStart = intervalStart.minusDays(satellite.durationCalc)
        val baseWindowStop = intervalStop.minusDays(satellite.durationCalc)
        val baseSlot = SlotEntity(
            slotId = 100L,
            slotNum = 7L,
            cycle = 0L,
            satelliteId = satellite.id,
            coveringType = 0,
            tn = LocalDateTime.of(2026, 3, 31, 10, 0),
            tk = LocalDateTime.of(2026, 3, 31, 10, 10),
            roll = 5.0,
            contour = "POLYGON ((0 0, 0 1, 1 1, 0 0))",
            revolution = 11L,
            revolutionSign = RevolutionSign.ASC.toString(),
            latitude = 55.75,
            longitude = 37.62
        )

        `when`(
            slotRepository.findBySatelliteIdAndTkAfterAndTnBeforeOrderByTnAscTkAscSatelliteIdAscSlotNumAsc(
                eqLongValue(satellite.id),
                eqDateTimeValue(baseWindowStart),
                eqDateTimeValue(baseWindowStop)
            )
        ).thenReturn(listOf(baseSlot))
        `when`(
            slotRepository.findBySatelliteIdAndTkAfterAndTnBeforeOrderByTnAscTkAscSatelliteIdAscSlotNumAsc(
                eqLongValue(satellite.id),
                anyDateTimeValue(),
                anyDateTimeValue()
            )
        ).thenReturn(emptyList())
        `when`(
            slotRepository.findBySatelliteIdAndTkAfterAndTnBeforeOrderByTnAscTkAscSatelliteIdAscSlotNumAsc(
                eqLongValue(satellite.id),
                eqDateTimeValue(baseWindowStart),
                eqDateTimeValue(baseWindowStop)
            )
        ).thenReturn(listOf(baseSlot))

        val actual = service.allByInterval(satellite.id, intervalStart, intervalStop)

        assertEquals(1, actual.size)
        assertEquals(1L, actual.first().cycle)
        assertEquals(LocalDateTime.of(2026, 4, 16, 10, 0), actual.first().tn)
        assertEquals(LocalDateTime.of(2026, 4, 16, 10, 10), actual.first().tk)
        assertEquals(11L + satellite.cycleRevs, actual.first().revolution)
    }

    @Test
    fun `allByInterval rejects reversed interval`() {
        val service = slotServiceFixture(mock(SlotRepository::class.java), TestSatelliteImpl(id = 22L))

        assertFailsWith<CustomValidationException> {
            service.allByInterval(
                satelliteId = 22L,
                timeStart = LocalDateTime.of(2026, 4, 17, 0, 0),
                timeStop = LocalDateTime.of(2026, 4, 16, 0, 0)
            )
        }
    }

    @Test
    fun `slotCalculationSummaries returns empty summary for satellites without slots`() {
        val slotRepository = mock(SlotRepository::class.java)
        val service = slotServiceFixture(slotRepository, TestSatelliteImpl(id = 22L))
        val summary = TestSlotCalculationSummaryProjection(
            satelliteId = 22L,
            slotCount = 3L,
            minTime = LocalDateTime.of(2026, 4, 20, 0, 0),
            maxTime = LocalDateTime.of(2026, 4, 21, 0, 0),
            minRevolution = 10L,
            maxRevolution = 15L
        )
        `when`(slotRepository.slotCalculationSummary(22L)).thenReturn(summary)
        `when`(slotRepository.slotCalculationSummary(44L)).thenReturn(
            TestSlotCalculationSummaryProjection(
                satelliteId = 44L,
                slotCount = 0L,
                minTime = null,
                maxTime = null,
                minRevolution = null,
                maxRevolution = null
            )
        )

        val actual = service.slotCalculationSummaries(listOf(22L, 44L, 22L))

        assertEquals(2, actual.size)
        assertEquals(true, actual[0].calculated)
        assertEquals(5L, actual[0].revolutionInterval)
        assertEquals(44L, actual[1].satelliteId)
        assertEquals(false, actual[1].calculated)
        assertEquals(0L, actual[1].slotCount)
    }

    private fun slotServiceFixture(slotRepository: SlotRepository, satellite: AbstractSatellite): SlotService {
        val service = SlotService()
        ReflectionTestUtils.setField(service, "slotRepository", slotRepository)
        ReflectionTestUtils.setField(service, "bookedSlotsRepository", mock(BookedSlotsRepository::class.java))
        ReflectionTestUtils.setField(service, "requestRepository", mock(BookedRequestRepository::class.java))
        ReflectionTestUtils.setField(service, "earthService", mock(EarthService::class.java))
        ReflectionTestUtils.setField(service, "dataSource", mock(DataSource::class.java))
        ReflectionTestUtils.setField(service, "satelliteCatalogClient", testSatelliteCatalogClient(listOf(satellite)))
        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
    }
}
