package space.nstart.pcp.pcp_coverage_scheme_service.service

import org.junit.jupiter.api.Test
import space.nstart.pcp.pcp_coverage_scheme_service.client.CoverageBallisticsClient
import space.nstart.pcp.pcp_coverage_scheme_service.client.CoverageSatelliteClient
import space.nstart.pcp.pcp_coverage_scheme_service.configuration.CoverageSchemeProperties
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ExactTimePositionRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.FlightLineDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.ObjViewRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.OrbPointDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.PointViewParamDTO
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import space.nstart.pcp.pcp_types_lib.dto.coverage.CoverageSchemeCalculateRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.satellite.SatelliteInfoDTO
import java.time.LocalDateTime
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class CoverageSchemeCalculationServiceTest {

    private val geometrySupport = GeometrySupport()

    @Test
    fun `calculate returns all mplSquare observations with strip intersection contours`() {
        val observation = SquareViewParamDTO(
            noradId = 11L,
            timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
            timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5),
            revSignBegin = RevolutionSign.ASC,
            revSignEnd = RevolutionSign.ASC,
            gammaMin = 0.0,
            gammaMax = 0.0
        )
        val fakeClient = FakeBallisticsClient(
            observations = listOf(observation),
            flightLinesBySatellite = mapOf(
                11L to listOf(
                    flightPoint(LocalDateTime.of(2026, 4, 14, 10, 0), lat = 0.0, lon = 196.0, leftLon = 195.5, rightLon = 196.5),
                    flightPoint(LocalDateTime.of(2026, 4, 14, 10, 5), lat = 2.0, lon = 196.0, leftLon = 195.5, rightLon = 196.5)
                )
            ),
            orbitalPointsBySatellite = mapOf(11L to sampleOrbitalPoints())
        )
        val service = buildService(fakeClient)

        val response = service.calculate(
            CoverageSchemeCalculateRequestDTO(
                polygonWkt = "POLYGON ((195 0, 197 0, 197 2, 195 2, 195 0))",
                satelliteIds = listOf(11L),
                timeStart = LocalDateTime.of(2026, 4, 14, 9, 55),
                timeEnd = LocalDateTime.of(2026, 4, 14, 10, 10)
            )
        )

        assertEquals(1, response.observations.size)
        assertEquals(observation.noradId, response.observations.first().observation.noradId)
        assertEquals(observation.timeBegin, response.observations.first().observation.timeBegin)
        assertEquals(observation.timeEnd, response.observations.first().observation.timeEnd)
        assertEquals(1, response.observations.first().intersectionContourWkts.size)
        assertTrue(response.observations.first().intersectionContourWkts.first().contains("195.5 0"))
        assertTrue(response.observations.first().intersectionContourWkts.first().contains("196.5 2"))
        assertEquals(1, response.coverageScheme.size)
        assertEquals(0.0, response.coverageScheme.first().roll)
        assertEquals(observation.timeBegin, response.coverageScheme.first().tn)
        assertEquals(observation.timeEnd, response.coverageScheme.first().tk)
        assertTrue(response.coverageScheme.first().contour.startsWith("POLYGON"))
    }

    @Test
    fun `calculate keeps observation when flight line does not produce polygon`() {
        val observation = SquareViewParamDTO(
            noradId = 11L,
            timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
            timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5)
        )
        val service = buildService(
            FakeBallisticsClient(
                observations = listOf(observation),
                flightLinesBySatellite = mapOf(
                    11L to listOf(
                        flightPoint(LocalDateTime.of(2026, 4, 14, 10, 0), lat = 0.0, lon = 196.0, leftLon = 195.5, rightLon = 196.5)
                    )
                ),
                orbitalPointsBySatellite = emptyMap()
            )
        )

        val response = service.calculate(
            CoverageSchemeCalculateRequestDTO(
                polygonWkt = "POLYGON ((195 0, 197 0, 197 2, 195 2, 195 0))",
                satelliteIds = listOf(11L),
                timeStart = LocalDateTime.of(2026, 4, 14, 9, 55),
                timeEnd = LocalDateTime.of(2026, 4, 14, 10, 10)
            )
        )

        assertEquals(1, response.observations.size)
        assertTrue(response.observations.first().intersectionContourWkts.isEmpty())
        assertTrue(response.coverageScheme.isEmpty())
    }

    @Test
    fun `calculate preserves inner exclusion zone inside strip contour`() {
        val observation = SquareViewParamDTO(
            noradId = 11L,
            timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
            timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5)
        )
        val service = buildService(
            FakeBallisticsClient(
                observations = listOf(observation),
                flightLinesBySatellite = mapOf(
                    11L to listOf(
                        flightPoint(
                            LocalDateTime.of(2026, 4, 14, 10, 0),
                            lat = 0.0,
                            lon = 196.0,
                            leftLon = 195.5,
                            rightLon = 196.5,
                            innerLeftLon = 195.9,
                            innerRightLon = 196.1
                        ),
                        flightPoint(
                            LocalDateTime.of(2026, 4, 14, 10, 5),
                            lat = 2.0,
                            lon = 196.0,
                            leftLon = 195.5,
                            rightLon = 196.5,
                            innerLeftLon = 195.9,
                            innerRightLon = 196.1
                        )
                    )
                ),
                orbitalPointsBySatellite = mapOf(11L to sampleOrbitalPoints())
            )
        )

        val response = service.calculate(
            CoverageSchemeCalculateRequestDTO(
                polygonWkt = "POLYGON ((195 0, 197 0, 197 2, 195 2, 195 0))",
                satelliteIds = listOf(11L),
                timeStart = LocalDateTime.of(2026, 4, 14, 9, 55),
                timeEnd = LocalDateTime.of(2026, 4, 14, 10, 10)
            )
        )

        val contours = response.observations.first().intersectionContourWkts
        assertEquals(2, contours.size)
        assertTrue(contours.any { it.contains("195.5 0") && it.contains("195.9 2") })
        assertTrue(contours.any { it.contains("196.1 0") && it.contains("196.5 2") })
        assertEquals(1, response.coverageScheme.size)
    }

    @Test
    fun `calculate normalizes ballistics flight line to standard band for zero crossing target`() {
        val observation = SquareViewParamDTO(
            noradId = 11L,
            timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
            timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5),
            gammaMin = 0.0,
            gammaMax = 0.0
        )
        val service = buildService(
            FakeBallisticsClient(
                observations = listOf(observation),
                flightLinesBySatellite = mapOf(
                    11L to listOf(
                        flightPoint(
                            time = LocalDateTime.of(2026, 4, 14, 10, 0),
                            lat = 0.0,
                            lon = 0.0,
                            leftLon = 359.5,
                            rightLon = 0.5
                        ),
                        flightPoint(
                            time = LocalDateTime.of(2026, 4, 14, 10, 5),
                            lat = 2.0,
                            lon = 0.0,
                            leftLon = 359.5,
                            rightLon = 0.5
                        )
                    )
                ),
                orbitalPointsBySatellite = mapOf(11L to sampleOrbitalPoints())
            )
        )

        val response = service.calculate(
            CoverageSchemeCalculateRequestDTO(
                polygonWkt = "POLYGON ((-1 0, 1 0, 1 2, -1 2, -1 0))",
                satelliteIds = listOf(11L),
                timeStart = LocalDateTime.of(2026, 4, 14, 9, 55),
                timeEnd = LocalDateTime.of(2026, 4, 14, 10, 10)
            )
        )

        assertEquals(1, response.observations.size)
        assertEquals(1, response.observations.first().intersectionContourWkts.size)
        assertTrue(response.observations.first().intersectionContourWkts.first().contains("-0.5 0"))
        assertTrue(response.observations.first().intersectionContourWkts.first().contains("0.5 2"))
    }

    @Test
    fun `calculate returns observations in stable order even if mplSquare order changes`() {
        val first = SquareViewParamDTO(
            noradId = 11L,
            revolutionBegin = 3L,
            timeBegin = LocalDateTime.of(2026, 4, 14, 10, 0),
            timeEnd = LocalDateTime.of(2026, 4, 14, 10, 5),
            gammaMin = 1.0,
            gammaMax = 3.0
        )
        val second = SquareViewParamDTO(
            noradId = 22L,
            revolutionBegin = 2L,
            timeBegin = LocalDateTime.of(2026, 4, 14, 9, 55),
            timeEnd = LocalDateTime.of(2026, 4, 14, 10, 1),
            gammaMin = -1.0,
            gammaMax = 2.0
        )
        val service = buildService(
            FakeBallisticsClient(
                observations = listOf(first, second),
                flightLinesBySatellite = emptyMap(),
                orbitalPointsBySatellite = emptyMap()
            )
        )

        val response = service.calculate(
            CoverageSchemeCalculateRequestDTO(
                polygonWkt = "POLYGON ((195 0, 197 0, 197 2, 195 2, 195 0))",
                satelliteIds = listOf(22L, 11L),
                timeStart = LocalDateTime.of(2026, 4, 14, 9, 55),
                timeEnd = LocalDateTime.of(2026, 4, 14, 10, 10)
            )
        )

        assertEquals(listOf(11L, 22L), response.observations.map { it.observation.noradId })
        assertEquals(
            listOf(
                LocalDateTime.of(2026, 4, 14, 10, 0),
                LocalDateTime.of(2026, 4, 14, 9, 55)
            ),
            response.observations.map { it.observation.timeBegin }
        )
    }

    private class FakeBallisticsClient(
        private val observations: List<SquareViewParamDTO>,
        private val flightLinesBySatellite: Map<Long, List<FlightLineDTO>>,
        private val orbitalPointsBySatellite: Map<Long, List<OrbPointDTO>>
    ) : CoverageBallisticsClient {
        override fun mplSquare(req: ObjViewRequestDTO): List<SquareViewParamDTO> = observations

        override fun mplPoint(req: ObjViewRequestDTO): List<PointViewParamDTO> = emptyList()

        override fun flightLine(
            satelliteId: Long,
            timeStart: LocalDateTime,
            timeStop: LocalDateTime
        ): List<FlightLineDTO> = flightLinesBySatellite[satelliteId].orEmpty()
            .filter { point -> !point.time.isBefore(timeStart) && !point.time.isAfter(timeStop) }

        override fun exactTime(
            satelliteId: Long,
            request: ExactTimePositionRequestDTO
        ): List<OrbPointDTO> = orbitalPointsBySatellite[satelliteId].orEmpty()
            .filter { point -> !point.time.isBefore(request.time) && !point.time.isAfter(request.timeStop ?: request.time) }
    }

    private fun flightPoint(
        time: LocalDateTime,
        lat: Double,
        lon: Double,
        leftLon: Double,
        rightLon: Double,
        innerLeftLon: Double = lon,
        innerRightLon: Double = lon
    ) = FlightLineDTO(
        time = time,
        lat = lat,
        long = lon,
        latLeft = lat,
        longLeft = leftLon,
        latInnerLeft = lat,
        longInnerLeft = innerLeftLon,
        latInnerRight = lat,
        longInnerRight = innerRightLon,
        latRight = lat,
        longRight = rightLon
    )

    private fun buildService(fakeClient: FakeBallisticsClient) = CoverageSchemeCalculationService(
        ballisticsClient = fakeClient,
        coverageSchemeBuilderService = CoverageSchemeBuilderService(fakeClient, FakeSatelliteClient(), geometrySupport),
        geometrySupport = geometrySupport,
        properties = CoverageSchemeProperties(flightLinePaddingSeconds = 0)
    )

    private class FakeSatelliteClient : CoverageSatelliteClient {
        override fun satellite(satelliteId: Long): SatelliteInfoDTO =
            SatelliteInfoDTO(noradId = satelliteId, captureAngle = 1.5)
    }

    private fun sampleOrbitalPoints() = listOf(
        OrbPointDTO(
            time = LocalDateTime.of(2026, 4, 14, 10, 0),
            revolution = 1L,
            vx = -401.529,
            vy = 1413.431,
            vz = 7547.696,
            x = -6603039.949,
            y = -1870023.148,
            z = 0.0
        ),
        OrbPointDTO(
            time = LocalDateTime.of(2026, 4, 14, 10, 1),
            revolution = 1L,
            vx = -420.0,
            vy = 1390.0,
            vz = 7540.0,
            x = -6595000.0,
            y = -1860000.0,
            z = 452000.0
        )
    )
}
