package space.nstart.pcp.slots_service.service

import ballistics.types.EarthType
import ballistics.utils.astro.AstronomerJ2000
import ballistics.utils.fromDateTime
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyList
import org.mockito.ArgumentMatchers.anyLong
import org.mockito.ArgumentMatchers.anyString
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.requests.slots.SlotCoverageBatchRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageTargetDTO
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.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.LocalDateTime
import javax.sql.DataSource
import kotlin.math.PI
import kotlin.math.abs
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class SlotServiceSunFilterTest {

    private fun anyStringValue(): String = anyString()
    private fun <T> anyListValue(): List<T> = anyList<T>() ?: emptyList()
    private fun anyLongValue(): Long = anyLong()
    private fun anyIntValue(): Int = anyInt()
    private fun <T> eqValue(value: T): T = eq(value) ?: value

    @Test
    fun `polySlotsBatch does not apply sun filter when sun is null`() {
        val fixture = slotServiceFixture()
        val satellite = fixture.satellite
        val templateStart = satellite.tnCalc.plusHours(2)
        val slot = templateSlot(satellite, templateStart)

        stubSingleSlotSpatialFetch(fixture, satellite.id, slot)
        `when`(fixture.bookedSlotsRepository.findBySlot_SatelliteIdInAndCycleBetween(eqValue(listOf(satellite.id)), anyLongValue(), anyLongValue()))
            .thenReturn(emptyList())

        val response = fixture.service.polySlotsBatch(
            request(
                satellite = satellite,
                timeStart = templateStart.minusMinutes(1),
                timeStop = templateStart.plusMinutes(11),
                sun = null
            )
        )

        assertEquals(1, response.size)
        assertEquals(1, response.single().slots.size)
    }

    @Test
    fun `polySlotsBatch drops slot when sun angle is less than or equal to requested threshold`() {
        val fixture = slotServiceFixture()
        val satellite = fixture.satellite
        val templateStart = satellite.tnCalc.plusHours(2)
        val slot = templateSlot(satellite, templateStart)
        val actualAngle = sunAngleDegrees(templateStart, slot.latitude, slot.longitude)

        stubSingleSlotSpatialFetch(fixture, satellite.id, slot)
        `when`(fixture.bookedSlotsRepository.findBySlot_SatelliteIdInAndCycleBetween(eqValue(listOf(satellite.id)), anyLongValue(), anyLongValue()))
            .thenReturn(emptyList())

        val response = fixture.service.polySlotsBatch(
            request(
                satellite = satellite,
                timeStart = templateStart.minusMinutes(1),
                timeStop = templateStart.plusMinutes(11),
                sun = actualAngle
            )
        )

        assertTrue(response.single().slots.isEmpty())
    }

    @Test
    fun `polySlotsBatch keeps slot when sun angle is above requested threshold`() {
        val fixture = slotServiceFixture()
        val satellite = fixture.satellite
        val templateStart = satellite.tnCalc.plusHours(2)
        val slot = templateSlot(satellite, templateStart)
        val actualAngle = sunAngleDegrees(templateStart, slot.latitude, slot.longitude)

        stubSingleSlotSpatialFetch(fixture, satellite.id, slot)
        `when`(fixture.bookedSlotsRepository.findBySlot_SatelliteIdInAndCycleBetween(eqValue(listOf(satellite.id)), anyLongValue(), anyLongValue()))
            .thenReturn(emptyList())

        val response = fixture.service.polySlotsBatch(
            request(
                satellite = satellite,
                timeStart = templateStart.minusMinutes(1),
                timeStop = templateStart.plusMinutes(11),
                sun = actualAngle - 0.1
            )
        )

        assertEquals(1, response.single().slots.size)
    }

    @Test
    fun `polySlotsBatch uses actual slot time when filtering by sun`() {
        val fixture = slotServiceFixture()
        val satellite = fixture.satellite
        val templateStart = satellite.tnCalc.plusHours(2)
        val slot = templateSlot(satellite, templateStart)
        val (cycle, templateAngle, actualAngle) = findCycleWithDifferentSunAngle(satellite, slot)
        val actualStart = templateStart.plusDays(cycle * satellite.durationCalc)
        val actualStop = actualStart.plusMinutes(10)
        val requestedSun = (templateAngle + actualAngle) / 2.0

        stubSingleSlotSpatialFetch(fixture, satellite.id, slot)
        `when`(fixture.bookedSlotsRepository.findBySlot_SatelliteIdInAndCycleBetween(eqValue(listOf(satellite.id)), anyLongValue(), anyLongValue()))
            .thenReturn(emptyList())

        val response = fixture.service.polySlotsBatch(
            request(
                satellite = satellite,
                timeStart = actualStart.minusMinutes(1),
                timeStop = actualStop.plusMinutes(1),
                sun = requestedSun
            )
        )

        val slots = response.single().slots
        if (actualAngle > templateAngle) {
            assertEquals(1, slots.size)
            assertEquals(actualStart, slots.single().tn)
        } else {
            assertTrue(slots.isEmpty())
        }
    }

    @Test
    fun `polySlotsBatch keeps target list when sun filter removes all slots`() {
        val fixture = slotServiceFixture()
        val satellite = fixture.satellite
        val templateStart = satellite.tnCalc.plusHours(2)
        val slot = templateSlot(satellite, templateStart)
        val actualAngle = sunAngleDegrees(templateStart, slot.latitude, slot.longitude)

        stubSingleSlotSpatialFetch(fixture, satellite.id, slot)
        `when`(fixture.bookedSlotsRepository.findBySlot_SatelliteIdInAndCycleBetween(eqValue(listOf(satellite.id)), anyLongValue(), anyLongValue()))
            .thenReturn(emptyList())

        val response = fixture.service.polySlotsBatch(
            SlotCoverageBatchRequestDTO(
                items = listOf(
                    SlotCoverageTargetDTO(1L, "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"),
                    SlotCoverageTargetDTO(2L, "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))")
                ),
                timeStart = templateStart.minusMinutes(1),
                timeStop = templateStart.plusMinutes(11),
                cov = false,
                satellites = listOf(satellite.id),
                sun = actualAngle
            )
        )

        assertEquals(2, response.size)
        assertTrue(response.all { it.slots.isEmpty() })
    }

    private fun slotServiceFixture(): SlotServiceFixture {
        val slotRepository = mock(SlotRepository::class.java)
        val bookedSlotsRepository = mock(BookedSlotsRepository::class.java)
        val satellite = TestSatelliteImpl(id = 22L)

        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", mock(EarthService::class.java))
        ReflectionTestUtils.setField(service, "dataSource", mock(DataSource::class.java))
        ReflectionTestUtils.setField(service, "satelliteCatalogClient", testSatelliteCatalogClient(listOf(satellite)))
        return SlotServiceFixture(service, slotRepository, bookedSlotsRepository, satellite)
    }

    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 request(
        satellite: AbstractSatellite,
        timeStart: LocalDateTime,
        timeStop: LocalDateTime,
        sun: Double?
    ) = SlotCoverageBatchRequestDTO(
        items = listOf(SlotCoverageTargetDTO(1L, "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))")),
        timeStart = timeStart,
        timeStop = timeStop,
        cov = false,
        satellites = listOf(satellite.id),
        sun = sun
    )

    private fun templateSlot(
        satellite: AbstractSatellite,
        templateStart: LocalDateTime
    ) = SlotEntity(
        slotId = 101L,
        slotNum = 1L,
        cycle = 0L,
        satelliteId = satellite.id,
        coveringType = 0,
        tn = templateStart,
        tk = templateStart.plusMinutes(10),
        roll = 10.0,
        contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
        revolution = 500L,
        revolutionSign = "ASC",
        latitude = 55.75,
        longitude = 37.62
    )

    private fun stubSingleSlotSpatialFetch(
        fixture: SlotServiceFixture,
        satelliteId: Long,
        slot: SlotEntity
    ) {
        `when`(
            fixture.slotRepository.findTargetSlotRows(
                anyListValue(),
                anyIntValue()
            )
        ).thenReturn(
            listOf(
                BatchCoverageSlotRow(
                    targetId = 1L,
                    slotId = slot.slotId ?: -1L,
                    slotNum = slot.slotNum,
                    cycle = slot.cycle,
                    satelliteId = slot.satelliteId,
                    coveringType = slot.coveringType,
                    tn = slot.tn,
                    tk = slot.tk,
                    roll = slot.roll,
                    contour = slot.contour,
                    revolution = slot.revolution,
                    revolutionSign = slot.revolutionSign,
                    latitude = slot.latitude,
                    longitude = slot.longitude
                )
            )
        )
    }

    private fun findCycleWithDifferentSunAngle(
        satellite: AbstractSatellite,
        slot: SlotEntity
    ): Triple<Long, Double, Double> {
        val templateAngle = sunAngleDegrees(slot.tn, slot.latitude, slot.longitude)
        for (cycle in 1L..40L) {
            val actualTime = slot.tn.plusDays(cycle * satellite.durationCalc)
            val actualAngle = sunAngleDegrees(actualTime, slot.latitude, slot.longitude)
            if (abs(actualAngle - templateAngle) > 0.5) {
                return Triple(cycle, templateAngle, actualAngle)
            }
        }
        error("Unable to find cycle with distinct sun angle for actual slot time test")
    }

    private fun sunAngleDegrees(time: LocalDateTime, latitudeDegrees: Double, longitudeDegrees: Double): Double {
        val astronomer = AstronomerJ2000(EarthType.PZ90d02)
        val position = astronomer.earth.blh2xyz(
            latitudeDegrees / 180.0 * PI,
            longitudeDegrees / 180.0 * PI,
            0.0
        )
        return astronomer.sunAngle(fromDateTime(time), position) * 180.0 / PI
    }

    private data class SlotServiceFixture(
        val service: SlotService,
        val slotRepository: SlotRepository,
        val bookedSlotsRepository: BookedSlotsRepository,
        val satellite: AbstractSatellite
    )
}
