package space.nstart.pcp.complan.apl

import org.locationtech.jts.io.WKTReader
import space.nstart.pcp.complan.settings.SettingsDTO
import space.nstart.pcp.complan.spacecraft.SCConstraints
import space.nstart.pcp.complan.spacecraft.SpaceCraft
import space.nstart.pcp.complan.types.DynamicPlanRevolutionMode
import space.nstart.pcp.complan.types.IntervalItemDTO
import space.nstart.pcp.complan.types.SurveyContour
import space.nstart.pcp.pcp_types_lib.dto.ballistics.RevolutionSign
import space.nstart.pcp.pcp_types_lib.dto.ballistics.SquareViewParamDTO
import java.time.LocalDateTime
import java.util.UUID
import kotlin.test.Test
import kotlin.test.assertEquals

class BasicPlanCalculatorTest {

    @Test
    fun `selects only satellites present in catalog`() {
        val result = selectSatellitesForCalculation(
            requestedSatelliteIds = listOf(56756L, 62138L, 99999L),
            catalogSatelliteIds = listOf(56756L, 62138L)
        )

        assertEquals(listOf(56756L, 62138L), result.existingIds)
        assertEquals(listOf(99999L), result.missingIds)
    }

    @Test
    fun `keeps requested order and removes duplicates`() {
        val result = selectSatellitesForCalculation(
            requestedSatelliteIds = listOf(62138L, 56756L, 62138L),
            catalogSatelliteIds = listOf(56756L, 62138L)
        )

        assertEquals(listOf(62138L, 56756L), result.existingIds)
        assertEquals(emptyList(), result.missingIds)
    }

    @Test
    fun `returns all requested satellites as missing when catalog has none of them`() {
        val result = selectSatellitesForCalculation(
            requestedSatelliteIds = listOf(1L, 2L, 3L),
            catalogSatelliteIds = listOf(56756L, 62138L)
        )

        assertEquals(emptyList(), result.existingIds)
        assertEquals(listOf(1L, 2L, 3L), result.missingIds)
    }

    @Test
    fun `route build uses all available MPL items sorted by gamma`() {
        val first = mplItem(noradId = 1L, gammaMin = -12.0)
        val second = mplItem(noradId = 2L, gammaMin = 4.0)
        val third = mplItem(noradId = 3L, gammaMin = -8.0)

        val result = selectMplItemsForRouteBuild(
            mplItems = listOf(first, second, third),
            settings = SettingsDTO()
        )

        assertEquals(listOf(second, third, first), result)
    }

    @Test
    fun `route build keeps existing gamma and revolution filters`() {
        val ascInGamma = mplItem(noradId = 1L, gammaMin = 3.0, gammaMax = 5.0, revSignBegin = RevolutionSign.ASC)
        val descInGamma = mplItem(noradId = 2L, gammaMin = 2.0, gammaMax = 4.0, revSignBegin = RevolutionSign.DESC)
        val ascOutOfGamma = mplItem(noradId = 3L, gammaMin = 20.0, gammaMax = 24.0, revSignBegin = RevolutionSign.ASC)

        val result = selectMplItemsForRouteBuild(
            mplItems = listOf(ascOutOfGamma, descInGamma, ascInGamma),
            settings = SettingsDTO(
                filterByGamma = true,
                gammaValue = 10.0,
                filterByRevolution = true,
                revolutionType = 0
            )
        )

        assertEquals(listOf(ascInGamma), result)
    }

    @Test
    fun `greedy route selection builds one route per interval`() {
        val interval = interval(
            mplItem(noradId = 1L, gammaMin = 2.0),
            mplItem(noradId = 2L, gammaMin = 1.0)
        )

        val result = selectGreedyRouteCandidates(
            intervals = listOf(interval),
            settings = SettingsDTO()
        )

        assertEquals(1, result.selectedCandidates.size)
        assertEquals(interval.id, result.selectedCandidates.single().intervalId)
        assertEquals(2L, result.selectedCandidates.single().satelliteId)
        assertEquals(emptyList(), result.failures)
    }

    @Test
    fun `greedy route selection prioritizes lower roll angle over earlier time`() {
        val earlierHighRollInterval = interval(
            mplItem(
                noradId = 1L,
                gammaMin = 12.0,
                timeBegin = LocalDateTime.of(2026, 5, 1, 0, 0),
                timeEnd = LocalDateTime.of(2026, 5, 1, 0, 1)
            )
        )
        val laterLowRollInterval = interval(
            mplItem(
                noradId = 2L,
                gammaMin = 1.0,
                timeBegin = LocalDateTime.of(2026, 5, 1, 0, 10),
                timeEnd = LocalDateTime.of(2026, 5, 1, 0, 11)
            )
        )

        val result = selectGreedyRouteCandidates(
            intervals = listOf(earlierHighRollInterval, laterLowRollInterval),
            settings = SettingsDTO()
        )

        assertEquals(
            listOf(laterLowRollInterval.id, earlierHighRollInterval.id),
            result.selectedCandidates.map { it.intervalId }
        )
        assertEquals(emptyList(), result.failures)
    }

    @Test
    fun `greedy route selection filters intervals by revolution mode`() {
        val ascInterval = interval(
            mplItem(noradId = 1L, gammaMin = 1.0, revSignBegin = RevolutionSign.ASC),
            revSign = RevolutionSign.ASC
        )
        val descInterval = interval(
            mplItem(noradId = 2L, gammaMin = 2.0, revSignBegin = RevolutionSign.DESC),
            revSign = RevolutionSign.DESC
        )

        val result = selectGreedyRouteCandidates(
            intervals = listOf(ascInterval, descInterval),
            settings = SettingsDTO(),
            revolutionMode = DynamicPlanRevolutionMode.DESC
        )

        assertEquals(listOf(descInterval.id), result.selectedCandidates.map { it.intervalId })
        assertEquals(emptyList(), result.failures)
    }

    @Test
    fun `interval build uses only requested revolution signs`() {
        assertEquals(listOf(RevolutionSign.ASC), revolutionSignsForIntervals(DynamicPlanRevolutionMode.ASC))
        assertEquals(listOf(RevolutionSign.DESC), revolutionSignsForIntervals(DynamicPlanRevolutionMode.DESC))
        assertEquals(
            listOf(RevolutionSign.DESC, RevolutionSign.ASC),
            revolutionSignsForIntervals(DynamicPlanRevolutionMode.BOTH)
        )
    }

    @Test
    fun `greedy route selection avoids simultaneous routes for same satellite`() {
        val firstInterval = interval(
            mplItem(
                noradId = 1L,
                gammaMin = 1.0,
                timeBegin = LocalDateTime.of(2026, 5, 1, 0, 0),
                timeEnd = LocalDateTime.of(2026, 5, 1, 0, 1)
            )
        )
        val secondInterval = interval(
            mplItem(
                noradId = 1L,
                gammaMin = 1.0,
                timeBegin = LocalDateTime.of(2026, 5, 1, 0, 1, 30),
                timeEnd = LocalDateTime.of(2026, 5, 1, 0, 2)
            ),
            mplItem(
                noradId = 2L,
                gammaMin = 2.0,
                timeBegin = LocalDateTime.of(2026, 5, 1, 0, 1, 30),
                timeEnd = LocalDateTime.of(2026, 5, 1, 0, 2)
            )
        )

        val result = selectGreedyRouteCandidates(
            intervals = listOf(secondInterval, firstInterval),
            settings = SettingsDTO()
        )

        assertEquals(listOf(1L, 2L), result.selectedCandidates.map { it.satelliteId })
        assertEquals(emptyList(), result.failures)
    }

    @Test
    fun `greedy route selection reports revolution limit failures`() {
        val firstInterval = interval(
            mplItem(
                noradId = 1L,
                gammaMin = 1.0,
                revolutionBegin = 10,
                timeBegin = LocalDateTime.of(2026, 5, 1, 0, 0),
                timeEnd = LocalDateTime.of(2026, 5, 1, 0, 4, 10)
            )
        )
        val secondInterval = interval(
            mplItem(
                noradId = 1L,
                gammaMin = 1.0,
                revolutionBegin = 10,
                timeBegin = LocalDateTime.of(2026, 5, 1, 0, 6),
                timeEnd = LocalDateTime.of(2026, 5, 1, 0, 8)
            )
        )

        val result = selectGreedyRouteCandidates(
            intervals = listOf(firstInterval, secondInterval),
            settings = SettingsDTO()
        )

        assertEquals(1, result.selectedCandidates.size)
        assertEquals(GreedyIntervalFailureReason.REVOLUTION_LIMIT_EXCEEDED, result.failures.single().reason)
    }

    @Test
    fun `greedy route selection reports daily limit failures`() {
        val firstInterval = interval(
            mplItem(
                noradId = 1L,
                gammaMin = 1.0,
                revolutionBegin = 10,
                timeBegin = LocalDateTime.of(2026, 5, 1, 0, 0),
                timeEnd = LocalDateTime.of(2026, 5, 1, 0, 4, 50)
            )
        )
        val secondInterval = interval(
            mplItem(
                noradId = 1L,
                gammaMin = 1.0,
                revolutionBegin = 11,
                timeBegin = LocalDateTime.of(2026, 5, 1, 0, 6),
                timeEnd = LocalDateTime.of(2026, 5, 1, 0, 8)
            )
        )

        val result = selectGreedyRouteCandidates(
            intervals = listOf(firstInterval, secondInterval),
            settings = SettingsDTO(),
            spacecraft = listOf(
                SpaceCraft(
                    id = 1L,
                    constraints = SCConstraints(maxDailyDurationSeconds = 300)
                )
            )
        )

        assertEquals(1, result.selectedCandidates.size)
        assertEquals(GreedyIntervalFailureReason.DAILY_LIMIT_EXCEEDED, result.failures.single().reason)
    }

    @Test
    fun `remaining request geometry filter keeps route when it still covers uncovered request territory`() {
        val requestGeometry = geometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))")
        val earlierRoute = route(
            noradId = 1L,
            time = LocalDateTime.of(2026, 5, 1, 0, 0),
            geometry = "POLYGON ((-1 0, -1 1, 0.5 1, 0.5 0, -1 0))"
        )
        val laterRoute = route(
            noradId = 2L,
            time = LocalDateTime.of(2026, 5, 1, 0, 10),
            geometry = "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"
        )

        val result = filterRoutesByRemainingRequestGeometry(listOf(earlierRoute, laterRoute), requestGeometry)

        assertEquals(listOf(earlierRoute, laterRoute), result)
    }

    @Test
    fun `remaining request geometry filter removes route after earlier route covers request territory`() {
        val requestGeometry = geometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))")
        val earlierRoute = route(
            noradId = 1L,
            time = LocalDateTime.of(2026, 5, 1, 0, 0),
            geometry = "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"
        )
        val laterRoute = route(
            noradId = 2L,
            time = LocalDateTime.of(2026, 5, 1, 0, 10),
            geometry = "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"
        )

        val result = filterRoutesByRemainingRequestGeometry(listOf(earlierRoute, laterRoute), requestGeometry)

        assertEquals(listOf(earlierRoute), result)
    }

    @Test
    fun `remaining request geometry filter keeps route with uncovered territory`() {
        val requestGeometry = geometry("POLYGON ((0 0, 0 2, 2 2, 2 0, 0 0))")
        val earlierRoute = route(
            noradId = 1L,
            time = LocalDateTime.of(2026, 5, 1, 0, 0),
            geometry = "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))"
        )
        val laterRoute = route(
            noradId = 2L,
            time = LocalDateTime.of(2026, 5, 1, 0, 10),
            geometry = "POLYGON ((1 0, 1 1, 2 1, 2 0, 1 0))"
        )

        val result = filterRoutesByRemainingRequestGeometry(listOf(earlierRoute, laterRoute), requestGeometry)

        assertEquals(listOf(earlierRoute, laterRoute), result)
    }

    @Test
    fun `remaining request geometry filter keeps only routes intersecting uncovered territory`() {
        val requestGeometry = geometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))")
        val earlyLeftRoute = route(
            noradId = 1L,
            time = LocalDateTime.of(2026, 5, 1, 0, 0),
            geometry = "POLYGON ((0 0, 0 1, 0.5 1, 0.5 0, 0 0))"
        )
        val duplicateLeftRoute = route(
            noradId = 2L,
            time = LocalDateTime.of(2026, 5, 1, 0, 10),
            geometry = "POLYGON ((0 0, 0 1, 0.5 1, 0.5 0, 0 0))"
        )
        val laterRightRoute = route(
            noradId = 3L,
            time = LocalDateTime.of(2026, 5, 1, 0, 20),
            geometry = "POLYGON ((0.5 0, 0.5 1, 1 1, 1 0, 0.5 0))"
        )

        val result = filterRoutesByRemainingRequestGeometry(
            routes = listOf(laterRightRoute, duplicateLeftRoute, earlyLeftRoute),
            requestGeometry = requestGeometry
        )

        assertEquals(listOf(earlyLeftRoute, laterRightRoute), result)
    }

    @Test
    fun `route coverage summary returns last route and covered area percent`() {
        val requestGeometry = geometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))")
        val earlierRoute = route(
            noradId = 1L,
            time = LocalDateTime.of(2026, 5, 1, 0, 0),
            geometry = "POLYGON ((0 0, 0 1, 0.5 1, 0.5 0, 0 0))"
        )
        val laterRoute = route(
            noradId = 2L,
            time = LocalDateTime.of(2026, 5, 10, 12, 0),
            geometry = "POLYGON ((0.5 0, 0.5 1, 1 1, 1 0, 0.5 0))"
        )

        val result = summarizeRouteCoverage(listOf(earlierRoute, laterRoute), requestGeometry)

        assertEquals(laterRoute.time, result.lastRouteStart)
        assertEquals(laterRoute.time.plusSeconds(60), result.lastRouteEnd)
        assertEquals(100.0, result.coveredAreaPercent ?: -1.0, 0.0001)
    }

    @Test
    fun `greedy math file filter keeps only selected satellites inside requested interval`() {
        val calculationStart = LocalDateTime.of(2026, 5, 1, 10, 0)
        val calculationEnd = LocalDateTime.of(2026, 5, 1, 11, 0)
        val validMpl = mplItem(
            noradId = 101L,
            gammaMin = 1.0,
            timeBegin = LocalDateTime.of(2026, 5, 1, 10, 5),
            timeEnd = LocalDateTime.of(2026, 5, 1, 10, 10)
        )
        val otherSatelliteMpl = mplItem(
            noradId = 202L,
            gammaMin = 1.0,
            timeBegin = LocalDateTime.of(2026, 5, 1, 10, 5),
            timeEnd = LocalDateTime.of(2026, 5, 1, 10, 10)
        )
        val startsBeforeIntervalMpl = mplItem(
            noradId = 101L,
            gammaMin = 1.0,
            timeBegin = LocalDateTime.of(2026, 5, 1, 9, 59),
            timeEnd = LocalDateTime.of(2026, 5, 1, 10, 10)
        )
        val endsAfterIntervalMpl = mplItem(
            noradId = 101L,
            gammaMin = 1.0,
            timeBegin = LocalDateTime.of(2026, 5, 1, 10, 50),
            timeEnd = LocalDateTime.of(2026, 5, 1, 11, 1)
        )
        val interval = interval(validMpl, otherSatelliteMpl, startsBeforeIntervalMpl, endsAfterIntervalMpl)

        val result = filterIntervalsForGreedyMathFile(
            intervals = listOf(interval),
            selectedSatelliteIds = listOf(101L),
            calculationStart = calculationStart,
            calculationEnd = calculationEnd
        )

        assertEquals(listOf(validMpl), result.single().mpl)
    }

    private fun mplItem(
        noradId: Long,
        gammaMin: Double,
        gammaMax: Double = gammaMin,
        revSignBegin: RevolutionSign = RevolutionSign.ASC,
        revolutionBegin: Long = 1,
        timeBegin: LocalDateTime = LocalDateTime.of(2026, 5, 1, 0, 0),
        timeEnd: LocalDateTime = LocalDateTime.of(2026, 5, 1, 0, 1)
    ) = SquareViewParamDTO(
        noradId = noradId,
        objectId = noradId.toString(),
        revolutionBegin = revolutionBegin,
        timeBegin = timeBegin,
        revSignBegin = revSignBegin,
        revolutionEnd = revolutionBegin,
        timeEnd = timeEnd,
        revSignEnd = revSignBegin,
        gammaMin = gammaMin,
        gammaMax = gammaMax
    )

    private fun interval(
        vararg mplItems: SquareViewParamDTO,
        revSign: RevolutionSign = RevolutionSign.ASC
    ) =
        IntervalItemDTO(
            id = UUID.randomUUID(),
            regionId = UUID.randomUUID(),
            revSign = revSign,
            geometry = "POLYGON EMPTY",
            mpl = mplItems.toMutableList()
        )

    private fun route(
        noradId: Long,
        time: LocalDateTime,
        geometry: String
    ) = SurveyContour(
        mpl = SquareViewParamDTO(noradId = noradId),
        time = time,
        duration = 60.0,
        rev = 1L,
        gamma = 0.0,
        geometry = geometry
    )

    private fun geometry(value: String) = WKTReader().read(value)
}
