package space.nstart.pcp.slots_service.service

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SatelliteOrbitAvailabilityDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteGroupDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteSummaryDTO
import space.nstart.pcp.slots_service.configuration.CustomErrorException
import java.time.LocalDateTime
import kotlin.math.abs
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue

class GroupStateServiceTest {

    private val satelliteCatalogService: SatelliteCatalogService = mock()
    private val ballisticsService: BallisticsService = mock()
    private val service = GroupStateService(satelliteCatalogService, ballisticsService)

    @Test
    fun `group summaries calculate availability intersection`() {
        doReturn(listOf(primaryGroup(), groupWithoutCommonInterval())).`when`(satelliteCatalogService).allGroups()
        doReturn(satelliteSummaries()).`when`(satelliteCatalogService).allSatellites()
        doReturn(orbitAvailability()).`when`(ballisticsService).orbitAvailability()

        val summaries = service.groupSummaries()

        assertEquals(2, summaries.size)
        assertNotNull(summaries[0].availableInterval)
        assertEquals(LocalDateTime.of(2026, 4, 24, 10, 30), summaries[0].availableInterval?.timeStart)
        assertEquals(LocalDateTime.of(2026, 4, 24, 11, 30), summaries[0].availableInterval?.timeStop)
        assertNull(summaries[1].availableInterval)
    }

    @Test
    fun `group state rejects time outside available interval`() {
        doReturn(primaryGroup()).`when`(satelliteCatalogService).group(71L)
        doReturn(satelliteSummaries()).`when`(satelliteCatalogService).allSatellites()
        doReturn(orbitAvailability()).`when`(ballisticsService).orbitAvailability()

        val exception = assertThrows<CustomErrorException> {
            service.groupState(71L, LocalDateTime.of(2026, 4, 24, 10, 15))
        }

        assertTrue(exception.message!!.contains("не входит в доступный интервал"))
    }

    @Test
    fun `group state builds pairwise matrix from orbital points`() {
        val calculationTime = LocalDateTime.of(2026, 4, 24, 10, 45)
        doReturn(primaryGroup()).`when`(satelliteCatalogService).group(71L)
        doReturn(satelliteSummaries()).`when`(satelliteCatalogService).allSatellites()
        doReturn(orbitAvailability()).`when`(ballisticsService).orbitAvailability()
        doReturn(firstOrbitalPoint(calculationTime)).`when`(ballisticsService).exactTimePoint(501L, calculationTime)
        doReturn(secondOrbitalPoint(calculationTime)).`when`(ballisticsService).exactTimePoint(502L, calculationTime)

        val state = service.groupState(71L, calculationTime)

        assertEquals(71L, state.groupId)
        assertEquals(calculationTime, state.calculationTime)
        assertEquals(2, state.satellites.size)
        assertEquals(2, state.matrix.size)
        state.satellites.forEach { satellite ->
            assertTrue(!satellite.omegabDeg.isNaN())
            assertTrue(!satellite.uDeg.isNaN())
            assertTrue(satellite.omegabDeg in 0.0..360.0)
            assertTrue(satellite.uDeg in 0.0..360.0)
        }

        val firstRow = state.matrix[0]
        val secondRow = state.matrix[1]
        assertEquals(0.0, firstRow.cells[0].deltaOmegabDeg)
        assertEquals(0.0, secondRow.cells[1].deltaUDeg)
        assertTrue(abs(firstRow.cells[1].deltaOmegabDeg + secondRow.cells[0].deltaOmegabDeg) < 0.0001)
        assertTrue(abs(firstRow.cells[1].deltaUDeg + secondRow.cells[0].deltaUDeg) < 0.0001)
    }

    private fun primaryGroup() = SatelliteGroupDTO(
        id = 71L,
        name = "KONDOR",
        satelliteIds = listOf(501L, 502L)
    )

    private fun groupWithoutCommonInterval() = SatelliteGroupDTO(
        id = 72L,
        name = "NO-INTERSECTION",
        satelliteIds = listOf(501L, 503L)
    )

    private fun satelliteSummaries() = listOf(
        SatelliteSummaryDTO(id = 501L, noradId = 56756L, code = "SAT-501", name = "Sat 501"),
        SatelliteSummaryDTO(id = 502L, noradId = 62138L, code = "SAT-502", name = "Sat 502"),
        SatelliteSummaryDTO(id = 503L, noradId = null, code = "SAT-503", name = "Sat 503")
    )

    private fun orbitAvailability() = listOf(
        SatelliteOrbitAvailabilityDTO(
            satelliteId = 501L,
            timeStart = LocalDateTime.of(2026, 4, 24, 10, 0),
            timeStop = LocalDateTime.of(2026, 4, 24, 12, 0)
        ),
        SatelliteOrbitAvailabilityDTO(
            satelliteId = 502L,
            timeStart = LocalDateTime.of(2026, 4, 24, 10, 30),
            timeStop = LocalDateTime.of(2026, 4, 24, 11, 30)
        )
    )

    private fun firstOrbitalPoint(time: LocalDateTime) = OrbPointDTO(
        time = time,
        revolution = 1L,
        vx = 941.8995373,
        vy = 1150.6919822,
        vz = 7544.9920840,
        x = -5357933.7872,
        y = 4328646.1787,
        z = 0.0
    )

    private fun secondOrbitalPoint(time: LocalDateTime) = OrbPointDTO(
        time = time,
        revolution = 1499L,
        vx = -272.89561674108205,
        vy = -1460.1016031205972,
        vz = 7539.209413654664,
        x = 6782429.138044466,
        y = -1205586.994919729,
        z = 0.0
    )
}
