package space.nstart.pcp.pcp_satellites_service.service

import org.junit.jupiter.api.Test
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.anyList
import org.mockito.ArgumentMatchers.anyMap
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.`when`
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.stubbing.Answer
import space.nstart.pcp.pcp_satellites_service.configuration.ComplexPlanProperties
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
import space.nstart.pcp.pcp_satellites_service.model.BLS
import space.nstart.pcp.pcp_satellites_service.model.LongTermMission
import space.nstart.pcp.pcp_satellites_service.model.SatelliteModel
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
import space.nstart.pcp.pcp_types_lib.dto.requests.CellRequestDOT
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
import space.nstart.pcp.pcp_types_lib.dto.requests.EarthCellWithRequestsDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.OccupiedIntervalDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SurveySlotDTO
import java.time.LocalDateTime
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class ComplexPlanCalculationServiceTest {

    private fun <T> anyListValue(): List<T> = anyList<T>() ?: emptyList()
    @Suppress("UNCHECKED_CAST")
    private fun anySurveyMap(): Map<Long, List<SurveyMode>> =
        anyMap<Long, List<SurveyMode>>() as Map<Long, List<SurveyMode>>? ?: emptyMap()
    private fun <T> eqValue(value: T): T = eq(value) ?: value
    private fun captureMap(captor: ArgumentCaptor<Map<Long, List<SurveyMode>>>): Map<Long, List<SurveyMode>> =
        captor.capture() ?: emptyMap()
    private fun anyLocalDateTime(): LocalDateTime = any(LocalDateTime::class.java) ?: LocalDateTime.MIN
    @Suppress("UNCHECKED_CAST")
    private fun anyMutableLongList(): MutableList<Long> =
        (any(MutableList::class.java) as MutableList<Long>?) ?: mutableListOf()
    @Suppress("UNCHECKED_CAST")
    private fun anyOccupiedIntervals(): List<OccupiedIntervalDTO> =
        (any(List::class.java) as List<OccupiedIntervalDTO>?) ?: emptyList()
    private fun captureOccupiedIntervals(captor: ArgumentCaptor<List<OccupiedIntervalDTO>>): List<OccupiedIntervalDTO> =
        captor.capture() ?: emptyList()
    private fun props(chunkSize: Int = 100) = ComplexPlanProperties().apply { batchChunkSize = chunkSize }
    private fun coverageBatchClient(): CoverageBatchClient =
        mock(CoverageBatchClient::class.java).also {
            `when`(it.source).thenReturn(CoverageCalculationSource.SLOTS)
        }
    private fun coverageResolver(vararg clients: CoverageCalculationClient) =
        CoverageCalculationClientResolver(clients.toList())

    @Test
    fun `process calculates first and then atomically replaces persisted result`() {
        val coverageBatchClient = coverageBatchClient()
        val earthGridService = mock(EarthGridService::class.java)
        val satelliteService = SatelliteService(testSatelliteCatalogClient())
        val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
        val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
        val surveyPlacementService = SurveyPlacementService()

        val service = ComplexPlanCalculationService(
            coverageBatchClient = coverageBatchClient,
            coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
            earthGridService = earthGridService,
            satelliteService = satelliteService,
            complexPlanQueryService = complexPlanQueryService,
            complexPlanPersistenceService = complexPlanPersistenceService,
            complexPlanProperties = props(),
            surveyPlacementService = surveyPlacementService
        )

        val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
        val intervalEnd = LocalDateTime.of(2026, 3, 27, 11, 0)
        val cell = EarthCellWithRequestsDTO(
            id = 100,
            num = 42,
            contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
            requests = emptyList()
        )

        `when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
            .thenReturn(emptyMap())
        `when`(earthGridService.cells(isNull(), isNull())).thenReturn(listOf(cell))
        `when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd))
            .thenReturn(
                mapOf(
                    22L to listOf(
                        SurveySlotDTO(
                            satelliteId = 22,
                            tn = intervalStart.plusMinutes(5),
                            tk = intervalStart.plusMinutes(15),
                            roll = 10.0,
                            contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
                            revolution = 501,
                            slotIds = listOf(9001)
                        )
                    )
                )
            )
        doReturn(
            mapOf(
                100L to listOf(
                    SlotDTO(
                        cycle = 0,
                        satelliteId = 22,
                        tn = intervalStart.plusMinutes(10),
                        tk = intervalStart.plusMinutes(20),
                        roll = 10.0,
                        contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))",
                        revolution = 501,
                        latitude = 10.0,
                        longitude = 20.0
                    )
                )
            )
        ).`when`(coverageBatchClient).coverageModes(
            anyListValue(),
            anyLocalDateTime(),
            anyLocalDateTime(),
            anyListValue(),
            anyOccupiedIntervals(),
            isNull(),
            isNull()
        )
        doReturn(SatelliteModeSaveResult(calculatedModesCount = 1, bookedModesCount = 1, affectedSatellitesCount = 1))
            .`when`(complexPlanPersistenceService).replaceModesForInterval(
                eqValue(listOf(22L)),
                eqValue(intervalStart),
                eqValue(intervalEnd),
                anySurveyMap(),
                isNull()
            )

        service.process(intervalStart, intervalEnd, listOf(22L))

        val inOrder = inOrder(complexPlanPersistenceService, complexPlanQueryService, coverageBatchClient)
        inOrder.verify(complexPlanQueryService).loadConstraintModes(listOf(22L), intervalStart, intervalEnd)
        inOrder.verify(coverageBatchClient).plannedModes(listOf(22L), intervalStart, intervalEnd)
        inOrder.verify(coverageBatchClient).coverageModes(
            anyListValue(),
            eqValue(intervalStart),
            eqValue(intervalEnd),
            eqValue(listOf(22L)),
            anyOccupiedIntervals(),
            isNull(),
            isNull()
        )
        inOrder.verify(complexPlanPersistenceService).replaceModesForInterval(
            eqValue(listOf(22L)),
            eqValue(intervalStart),
            eqValue(intervalEnd),
            anySurveyMap(),
            isNull()
        )

        val surveysCaptor = ArgumentCaptor.forClass(Map::class.java) as ArgumentCaptor<Map<Long, List<SurveyMode>>>
        verify(complexPlanPersistenceService).replaceModesForInterval(
            eqValue(listOf(22L)),
            eqValue(intervalStart),
            eqValue(intervalEnd),
            captureMap(surveysCaptor),
            isNull()
        )

        val savedSurvey = surveysCaptor.value.getValue(22L).single()
        assertEquals(SatelliteModeSource.SLOTS, savedSurvey.source)
        assertEquals(listOf(9001L), savedSurvey.bookedSlotIds)
        assertTrue(savedSurvey.time <= intervalStart.plusMinutes(5))
        assertTrue(savedSurvey.timeStop >= intervalStart.plusMinutes(15))
    }

    @Test
    fun `process returns covered area percent by survey area union`() {
        val coverageBatchClient = coverageBatchClient()
        val earthGridService = mock(EarthGridService::class.java)
        val satelliteService = SatelliteService(testSatelliteCatalogClient())
        val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
        val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
        val surveyPlacementService = SurveyPlacementService()

        val service = ComplexPlanCalculationService(
            coverageBatchClient = coverageBatchClient,
            coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
            earthGridService = earthGridService,
            satelliteService = satelliteService,
            complexPlanQueryService = complexPlanQueryService,
            complexPlanPersistenceService = complexPlanPersistenceService,
            complexPlanProperties = props(),
            surveyPlacementService = surveyPlacementService
        )

        val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
        val intervalEnd = LocalDateTime.of(2026, 3, 27, 11, 0)
        val requestContour = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))"
        val coveredContour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
        val cell = EarthCellWithRequestsDTO(
            id = 100,
            num = 42,
            contour = requestContour,
            requests = listOf(
                CellRequestDOT(
                    requestId = "request-1",
                    contour = requestContour
                )
            )
        )

        `when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
            .thenReturn(emptyMap())
        `when`(earthGridService.cells(isNull(), isNull())).thenReturn(listOf(cell))
        `when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
        doReturn(
            mapOf(
                100L to listOf(
                    SlotDTO(
                        cycle = 0,
                        satelliteId = 22,
                        tn = intervalStart.plusMinutes(10),
                        tk = intervalStart.plusMinutes(20),
                        roll = 10.0,
                        contour = coveredContour,
                        revolution = 501,
                        latitude = 10.0,
                        longitude = 20.0
                    )
                )
            )
        ).`when`(coverageBatchClient).coverageModes(
            anyListValue(),
            anyLocalDateTime(),
            anyLocalDateTime(),
            anyListValue(),
            anyOccupiedIntervals(),
            isNull(),
            isNull()
        )
        doReturn(SatelliteModeSaveResult(calculatedModesCount = 1, complanModesCount = 1, affectedSatellitesCount = 1))
            .`when`(complexPlanPersistenceService).replaceModesForInterval(
                eqValue(listOf(22L)),
                eqValue(intervalStart),
                eqValue(intervalEnd),
                anySurveyMap(),
                isNull()
            )

        val result = service.process(intervalStart, intervalEnd, listOf(22L))

        assertEquals(2.0, result.totalRequestsArea, 0.0001)
        assertEquals(1.0, result.coveredArea, 0.0001)
        assertEquals(50.0, result.coveredAreaPercent, 0.0001)
    }

    @Test
    fun `process calculates total requests area from all request cell fragments`() {
        val coverageBatchClient = coverageBatchClient()
        val earthGridService = mock(EarthGridService::class.java)
        val satelliteService = SatelliteService(testSatelliteCatalogClient())
        val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
        val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
        val surveyPlacementService = SurveyPlacementService()

        val service = ComplexPlanCalculationService(
            coverageBatchClient = coverageBatchClient,
            coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
            earthGridService = earthGridService,
            satelliteService = satelliteService,
            complexPlanQueryService = complexPlanQueryService,
            complexPlanPersistenceService = complexPlanPersistenceService,
            complexPlanProperties = props(),
            surveyPlacementService = surveyPlacementService
        )

        val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
        val intervalEnd = LocalDateTime.of(2026, 3, 27, 11, 0)
        val firstRequestFragment = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
        val secondRequestFragment = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))"
        val cells = listOf(
            EarthCellWithRequestsDTO(
                id = 100,
                num = 42,
                contour = firstRequestFragment,
                requests = listOf(
                    CellRequestDOT(
                        id = 1001,
                        requestId = "request-1",
                        contour = firstRequestFragment
                    )
                )
            ),
            EarthCellWithRequestsDTO(
                id = 101,
                num = 43,
                contour = secondRequestFragment,
                requests = listOf(
                    CellRequestDOT(
                        id = 1002,
                        requestId = "request-1",
                        contour = secondRequestFragment
                    )
                )
            )
        )

        `when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
            .thenReturn(emptyMap())
        `when`(earthGridService.cells(isNull(), isNull())).thenReturn(cells)
        `when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
        doReturn(emptyMap<Long, List<SlotDTO>>())
            .`when`(coverageBatchClient).coverageModes(
                anyListValue(),
                anyLocalDateTime(),
                anyLocalDateTime(),
                anyListValue(),
                anyOccupiedIntervals(),
                isNull(),
                isNull()
            )
        doReturn(SatelliteModeSaveResult())
            .`when`(complexPlanPersistenceService).replaceModesForInterval(
                eqValue(listOf(22L)),
                eqValue(intervalStart),
                eqValue(intervalEnd),
                anySurveyMap(),
                isNull()
            )

        val result = service.process(intervalStart, intervalEnd, listOf(22L))

        assertEquals(2.0, result.totalRequestsArea, 0.0001)
        assertEquals(0.0, result.coveredArea, 0.0001)
    }

    @Test
    fun `process requests coverage by chunks and stops before next chunk when resource is exhausted`() {
        val coverageBatchClient = coverageBatchClient()
        val earthGridService = mock(EarthGridService::class.java)
        val satelliteService = mock(SatelliteService::class.java)
        val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
        val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
        val surveyPlacementService = SurveyPlacementService()

        val service = ComplexPlanCalculationService(
            coverageBatchClient = coverageBatchClient,
            coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
            earthGridService = earthGridService,
            satelliteService = satelliteService,
            complexPlanQueryService = complexPlanQueryService,
            complexPlanPersistenceService = complexPlanPersistenceService,
            complexPlanProperties = props(chunkSize = 2),
            surveyPlacementService = surveyPlacementService
        )

        val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
        val intervalEnd = LocalDateTime.of(2026, 3, 27, 10, 0)
        val cells = listOf(
            EarthCellWithRequestsDTO(id = 1, num = 1, importance = 10.0, contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", requests = emptyList()),
            EarthCellWithRequestsDTO(id = 2, num = 2, importance = 9.0, contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))", requests = emptyList()),
            EarthCellWithRequestsDTO(id = 3, num = 3, importance = 8.0, contour = "POLYGON ((2 0, 3 0, 3 1, 2 1, 2 0))", requests = emptyList())
        )

        `when`(satelliteService.satellites).thenReturn(
            listOf(
                SatelliteModel(
                    satelliteId = 22L,
                    name = "T",
                    bls = BLS(durationMax = 900.0, dailyMaxDuration = 605.0, revolutionMaxDuration = 2_000.0)
                )
            )
        )
        `when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
            .thenReturn(emptyMap())
        `when`(earthGridService.cells(isNull(), isNull())).thenReturn(cells)
        `when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
        doReturn(
            mapOf(
                1L to listOf(
                    SlotDTO(
                        cycle = 0,
                        satelliteId = 22,
                        tn = intervalStart.plusMinutes(5),
                        tk = intervalStart.plusMinutes(15),
                        roll = 8.0,
                        contour = "POLYGON ((0 0, 2 0, 2 1, 0 1, 0 0))",
                        revolution = 501,
                        latitude = 10.0,
                        longitude = 20.0
                    )
                )
            )
        ).`when`(coverageBatchClient).coverageModes(
            anyListValue(),
            anyLocalDateTime(),
            anyLocalDateTime(),
            anyListValue(),
            anyOccupiedIntervals(),
            isNull(),
            isNull()
        )
        doReturn(SatelliteModeSaveResult()).`when`(complexPlanPersistenceService).replaceModesForInterval(
            eqValue(listOf(22L)),
            eqValue(intervalStart),
            eqValue(intervalEnd),
            anySurveyMap(),
            isNull()
        )

        service.process(intervalStart, intervalEnd, listOf(22L))

        verify(coverageBatchClient, times(1))
            .coverageModes(
                anyListValue(),
                eqValue(intervalStart),
                eqValue(intervalEnd),
                eqValue(listOf(22L)),
                anyOccupiedIntervals(),
                isNull(),
                isNull()
            )
    }

    @Test
    fun `process passes occupied intervals snapshot and refreshes it between chunks`() {
        val coverageBatchClient = coverageBatchClient()
        val earthGridService = mock(EarthGridService::class.java)
        val satelliteService = mock(SatelliteService::class.java)
        val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
        val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
        val surveyPlacementService = SurveyPlacementService()

        val service = ComplexPlanCalculationService(
            coverageBatchClient = coverageBatchClient,
            coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
            earthGridService = earthGridService,
            satelliteService = satelliteService,
            complexPlanQueryService = complexPlanQueryService,
            complexPlanPersistenceService = complexPlanPersistenceService,
            complexPlanProperties = props(chunkSize = 1),
            surveyPlacementService = surveyPlacementService
        )

        val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
        val intervalEnd = LocalDateTime.of(2026, 3, 27, 10, 0)
        val satellite = SatelliteModel(
            satelliteId = 22L,
            name = "T",
            longMission = LongTermMission(),
            bls = BLS(durationMax = 900.0, dailyMaxDuration = 2_000.0, revolutionMaxDuration = 2_000.0)
        )
        val firstCell = EarthCellWithRequestsDTO(
            id = 1,
            num = 101,
            importance = 10.0,
            contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
            requests = emptyList()
        )
        val secondCell = EarthCellWithRequestsDTO(
            id = 2,
            num = 102,
            importance = 9.0,
            contour = "POLYGON ((1 0, 2 0, 2 1, 1 1, 1 0))",
            requests = emptyList()
        )

        `when`(satelliteService.satellites).thenReturn(listOf(satellite))
        `when`(satelliteService.hasResources(eqValue(intervalStart), eqValue(intervalEnd), eqValue(listOf(22L)), anyMutableLongList()))
            .thenAnswer(Answer {
                val available = it.arguments[3] as MutableList<Long>
                available.clear()
                available += 22L
                true
            })
        `when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
            .thenReturn(
                mapOf(
                    22L to mutableListOf(
                        SurveyMode(
                            revolution = 500,
                            time = intervalStart.minusMinutes(20),
                            timeStop = intervalStart.minusMinutes(10),
                            roll = 12.5,
                            duration = 600.0,
                            contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
                            source = SatelliteModeSource.SLOTS
                        )
                    )
                )
            )
        `when`(earthGridService.cells(isNull(), isNull())).thenReturn(listOf(firstCell, secondCell))
        `when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
        doReturn(
            mapOf(
                1L to listOf(
                    SlotDTO(
                        cycle = 0,
                        satelliteId = 22,
                        tn = intervalStart.plusMinutes(5),
                        tk = intervalStart.plusMinutes(15),
                        roll = 8.0,
                        contour = "POLYGON ((2 0, 3 0, 3 1, 2 1, 2 0))",
                        revolution = 501,
                        latitude = 10.0,
                        longitude = 20.0
                    )
                )
            ),
            emptyMap<Long, List<SlotDTO>>()
        ).`when`(coverageBatchClient).coverageModes(
            anyListValue(),
            anyLocalDateTime(),
            anyLocalDateTime(),
            anyListValue(),
            anyOccupiedIntervals(),
            isNull(),
            isNull()
        )
        doReturn(SatelliteModeSaveResult()).`when`(complexPlanPersistenceService).replaceModesForInterval(
            eqValue(listOf(22L)),
            eqValue(intervalStart),
            eqValue(intervalEnd),
            anySurveyMap(),
            isNull()
        )

        service.process(intervalStart, intervalEnd, listOf(22L))

        val occupiedCaptor = ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<OccupiedIntervalDTO>>
        verify(coverageBatchClient, times(2)).coverageModes(
            anyListValue(),
            eqValue(intervalStart),
            eqValue(intervalEnd),
            eqValue(listOf(22L)),
            captureOccupiedIntervals(occupiedCaptor),
            isNull(),
            isNull()
        )

        val firstSnapshot = occupiedCaptor.allValues[0]
        val secondSnapshot = occupiedCaptor.allValues[1]

        assertEquals(1, firstSnapshot.size)
        assertEquals(2, secondSnapshot.size)
        assertTrue(secondSnapshot.any { it.roll == 8.0 && it.startTime == intervalStart.plusMinutes(5) && it.endTime == intervalStart.plusMinutes(15) })
    }

    @Test
    fun `process failure before replace does not touch persisted result`() {
        val coverageBatchClient = coverageBatchClient()
        val earthGridService = mock(EarthGridService::class.java)
        val satelliteService = mock(SatelliteService::class.java)
        val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
        val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
        val surveyPlacementService = SurveyPlacementService()

        val service = ComplexPlanCalculationService(
            coverageBatchClient = coverageBatchClient,
            coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
            earthGridService = earthGridService,
            satelliteService = satelliteService,
            complexPlanQueryService = complexPlanQueryService,
            complexPlanPersistenceService = complexPlanPersistenceService,
            complexPlanProperties = props(),
            surveyPlacementService = surveyPlacementService
        )

        val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
        val intervalEnd = LocalDateTime.of(2026, 3, 27, 12, 0)

        `when`(satelliteService.satellites).thenReturn(
            listOf(
                SatelliteModel(
                    satelliteId = 22L,
                    name = "T",
                    bls = BLS(durationMax = 900.0, dailyMaxDuration = 2_000.0, revolutionMaxDuration = 2_000.0)
                )
            )
        )
        `when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd))
            .thenReturn(emptyMap())
        org.mockito.Mockito.doThrow(RuntimeException("cells boom"))
            .`when`(earthGridService).cells(isNull(), isNull())

        kotlin.test.assertFailsWith<RuntimeException> {
            service.process(intervalStart, intervalEnd, listOf(22L))
        }

        verify(complexPlanPersistenceService, never()).replaceModesForInterval(
            eqValue(listOf(22L)),
            eqValue(intervalStart),
            eqValue(intervalEnd),
            anySurveyMap(),
            isNull()
        )
    }

    @Test
    fun `process passes sun filter to coverage batch client`() {
        val coverageBatchClient = coverageBatchClient()
        val earthGridService = mock(EarthGridService::class.java)
        val satelliteService = mock(SatelliteService::class.java)
        val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
        val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
        val surveyPlacementService = SurveyPlacementService()

        val service = ComplexPlanCalculationService(
            coverageBatchClient = coverageBatchClient,
            coverageCalculationClientResolver = coverageResolver(coverageBatchClient),
            earthGridService = earthGridService,
            satelliteService = satelliteService,
            complexPlanQueryService = complexPlanQueryService,
            complexPlanPersistenceService = complexPlanPersistenceService,
            complexPlanProperties = props(),
            surveyPlacementService = surveyPlacementService
        )

        val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
        val intervalEnd = LocalDateTime.of(2026, 3, 27, 12, 0)
        val requestedSun = 12.5
        val cell = EarthCellWithRequestsDTO(
            id = 100,
            num = 42,
            contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
            requests = emptyList()
        )
        var capturedSun: Double? = null

        `when`(satelliteService.satellites).thenReturn(
            listOf(
                SatelliteModel(
                    satelliteId = 22L,
                    name = "T",
                    bls = BLS(durationMax = 900.0, dailyMaxDuration = 2_000.0, revolutionMaxDuration = 2_000.0)
                )
            )
        )
        `when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
        `when`(earthGridService.cells(eqValue(2), eqValue(3))).thenReturn(listOf(cell))
        `when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
        org.mockito.Mockito.doAnswer { invocation ->
            capturedSun = invocation.arguments[5] as Double?
            emptyMap<Long, List<SlotDTO>>()
        }.`when`(coverageBatchClient).coverageModes(
            anyListValue(),
            anyLocalDateTime(),
            anyLocalDateTime(),
            anyListValue(),
            anyOccupiedIntervals(),
            org.mockito.ArgumentMatchers.nullable(Double::class.java),
            isNull()
        )
        doReturn(SatelliteModeSaveResult()).`when`(complexPlanPersistenceService).replaceModesForInterval(
            eqValue(listOf(22L)),
            eqValue(intervalStart),
            eqValue(intervalEnd),
            anySurveyMap(),
            isNull()
        )

        service.process(
            intervalStart = intervalStart,
            intervalEnd = intervalEnd,
            satelliteIds = listOf(22L),
            sun = requestedSun,
            countLat = 2,
            countLong = 3
        )

        verify(earthGridService).cells(2, 3)
        verify(coverageBatchClient, org.mockito.Mockito.atLeastOnce()).coverageModes(
            anyListValue(),
            anyLocalDateTime(),
            anyLocalDateTime(),
            anyListValue(),
            anyOccupiedIntervals(),
            org.mockito.ArgumentMatchers.nullable(Double::class.java),
            isNull()
        )
        assertEquals(requestedSun, capturedSun)
    }

    @Test
    fun `process uses coverage scheme client when requested`() {
        val coverageBatchClient = coverageBatchClient()
        val coverageSchemeClient = mock(CoverageCalculationClient::class.java)
        val earthGridService = mock(EarthGridService::class.java)
        val satelliteService = mock(SatelliteService::class.java)
        val complexPlanQueryService = mock(ComplexPlanQueryService::class.java)
        val complexPlanPersistenceService = mock(ComplexPlanPersistenceService::class.java)
        val surveyPlacementService = SurveyPlacementService()

        `when`(coverageSchemeClient.source).thenReturn(CoverageCalculationSource.COVERAGE_SCHEME)

        val service = ComplexPlanCalculationService(
            coverageBatchClient = coverageBatchClient,
            coverageCalculationClientResolver = coverageResolver(coverageBatchClient, coverageSchemeClient),
            earthGridService = earthGridService,
            satelliteService = satelliteService,
            complexPlanQueryService = complexPlanQueryService,
            complexPlanPersistenceService = complexPlanPersistenceService,
            complexPlanProperties = props(),
            surveyPlacementService = surveyPlacementService
        )

        val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
        val intervalEnd = LocalDateTime.of(2026, 3, 27, 12, 0)
        val cell = EarthCellWithRequestsDTO(
            id = 100,
            num = 42,
            contour = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
            requests = emptyList()
        )

        `when`(satelliteService.satellites).thenReturn(
            listOf(
                SatelliteModel(
                    satelliteId = 22L,
                    name = "T",
                    bls = BLS(durationMax = 900.0, dailyMaxDuration = 2_000.0, revolutionMaxDuration = 2_000.0)
                )
            )
        )
        `when`(complexPlanQueryService.loadConstraintModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
        `when`(earthGridService.cells(isNull(), isNull())).thenReturn(listOf(cell))
        `when`(coverageBatchClient.plannedModes(listOf(22L), intervalStart, intervalEnd)).thenReturn(emptyMap())
        doReturn(emptyMap<Long, List<SlotDTO>>()).`when`(coverageSchemeClient).coverageModes(
            anyListValue(),
            anyLocalDateTime(),
            anyLocalDateTime(),
            anyListValue(),
            anyOccupiedIntervals(),
            isNull(),
            isNull()
        )
        doReturn(SatelliteModeSaveResult()).`when`(complexPlanPersistenceService).replaceModesForInterval(
            eqValue(listOf(22L)),
            eqValue(intervalStart),
            eqValue(intervalEnd),
            anySurveyMap(),
            isNull()
        )

        service.process(
            intervalStart = intervalStart,
            intervalEnd = intervalEnd,
            satelliteIds = listOf(22L),
            coverageSource = CoverageCalculationSource.COVERAGE_SCHEME
        )

        verify(coverageBatchClient).plannedModes(listOf(22L), intervalStart, intervalEnd)
        verify(coverageSchemeClient).coverageModes(
            anyListValue(),
            eqValue(intervalStart),
            eqValue(intervalEnd),
            eqValue(listOf(22L)),
            anyOccupiedIntervals(),
            isNull(),
            isNull()
        )
        verify(coverageBatchClient, never()).coverageModes(
            anyListValue(),
            anyLocalDateTime(),
            anyLocalDateTime(),
            anyListValue(),
            anyOccupiedIntervals(),
            isNull(),
            isNull()
        )
    }
}
