package space.nstart.pcp.pcp_satellites_service.service

import org.junit.jupiter.api.Test
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import space.nstart.pcp.pcp_satellites_service.configuration.CustomValidationException
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunCreateDTO
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunStatisticsDTO
import space.nstart.pcp.pcp_satellites_service.dto.ComplexPlanRunResponseDTO
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunEntity
import space.nstart.pcp.pcp_satellites_service.entity.ComplexPlanRunStatus
import space.nstart.pcp.pcp_types_lib.dto.requests.CoverageCalculationSource
import space.nstart.pcp.pcp_types_lib.dto.requests.ComplexPlanProcessRequestDTO
import space.nstart.pcp.pcp_types_lib.dto.requests.slots.SlotCoverageStrategy
import java.time.LocalDateTime
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class ComplexPlanRunOrchestrationServiceTest {

    @Test
    fun `orchestration marks run failed when lock is unavailable`() {
        val persistenceService = mock(ComplexPlanRunPersistenceService::class.java)
        val queryService = mock(ComplexPlanRunQueryService::class.java)
        val calculationService = mock(ComplexPlanCalculationService::class.java)
        val satelliteService = mock(SatelliteService::class.java)
        val lockService = mock(ComplexPlanExecutionLockService::class.java)

        val orchestrationService = ComplexPlanRunOrchestrationService(
            complexPlanRunPersistenceService = persistenceService,
            complexPlanRunQueryService = queryService,
            complexPlanCalculationService = calculationService,
            satelliteService = satelliteService,
            complexPlanExecutionLockService = lockService
        )

        val request = ComplexPlanProcessRequestDTO(
            intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
            intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
            satelliteId = 22L
        )
        val createdRun = ComplexPlanRunEntity(
            id = 10L,
            status = ComplexPlanRunStatus.CREATED,
            intervalStart = request.intervalStart,
            intervalEnd = request.intervalEnd
        )

        doReturn(createdRun)
            .`when`(persistenceService)
            .createRun(
                ComplexPlanRunCreateDTO(
                    intervalStart = request.intervalStart,
                    intervalEnd = request.intervalEnd,
                    satelliteIds = listOf(22L),
                    requestedBy = "tester",
                    requestPayload = "{\"intervalStart\":\"2026-03-26T10:00:00\",\"intervalEnd\":\"2026-03-26T12:00:00\",\"satelliteId\":22,\"satelliteIds\":[],\"sun\":null,\"coverageSource\":null,\"countLat\":null,\"countLong\":null,\"coverageStrategy\":\"GREEDY\"}"
                )
            )
        doReturn(null).`when`(lockService).tryAcquire(listOf(22L))

        assertFailsWith<CustomValidationException> {
            orchestrationService.process(request, requestedBy = "tester")
        }

        verify(persistenceService).markFailed(
            10L,
            "Complex plan recalculation is already running for at least one requested satellite"
        )
        verify(calculationService, never()).process(
            10L,
            request.intervalStart,
            request.intervalEnd,
            listOf(22L),
            null,
            CoverageCalculationSource.SLOTS,
            null,
            null,
            SlotCoverageStrategy.GREEDY
        )
    }

    @Test
    fun `orchestration marks run failed when overlapping RUNNING run exists`() {
        val persistenceService = mock(ComplexPlanRunPersistenceService::class.java)
        val queryService = mock(ComplexPlanRunQueryService::class.java)
        val calculationService = mock(ComplexPlanCalculationService::class.java)
        val satelliteService = mock(SatelliteService::class.java)
        val lockService = mock(ComplexPlanExecutionLockService::class.java)
        val lockHandle = ComplexPlanExecutionLockService.ExecutionLockHandle {}

        val orchestrationService = ComplexPlanRunOrchestrationService(
            complexPlanRunPersistenceService = persistenceService,
            complexPlanRunQueryService = queryService,
            complexPlanCalculationService = calculationService,
            satelliteService = satelliteService,
            complexPlanExecutionLockService = lockService
        )

        val request = ComplexPlanProcessRequestDTO(
            intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
            intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
            satelliteId = 22L
        )
        val createdRun = ComplexPlanRunEntity(
            id = 10L,
            status = ComplexPlanRunStatus.CREATED,
            intervalStart = request.intervalStart,
            intervalEnd = request.intervalEnd
        )

        doReturn(createdRun)
            .`when`(persistenceService)
            .createRun(
                ComplexPlanRunCreateDTO(
                    intervalStart = request.intervalStart,
                    intervalEnd = request.intervalEnd,
                    satelliteIds = listOf(22L),
                    requestedBy = "tester",
                    requestPayload = "{\"intervalStart\":\"2026-03-26T10:00:00\",\"intervalEnd\":\"2026-03-26T12:00:00\",\"satelliteId\":22,\"satelliteIds\":[],\"sun\":null,\"coverageSource\":null,\"countLat\":null,\"countLong\":null,\"coverageStrategy\":\"GREEDY\"}"
                )
            )
        doReturn(lockHandle).`when`(lockService).tryAcquire(listOf(22L))
        doReturn(listOf(99L))
            .`when`(persistenceService)
            .findOverlappingRunningRunIds(listOf(22L), request.intervalStart, request.intervalEnd, 10L)

        assertFailsWith<CustomValidationException> {
            orchestrationService.process(request, requestedBy = "tester")
        }

        verify(persistenceService).markFailed(
            10L,
            "Complex plan recalculation is already running for overlapping interval, runIds=[99]"
        )
        verify(calculationService, never()).process(
            10L,
            request.intervalStart,
            request.intervalEnd,
            listOf(22L),
            null,
            CoverageCalculationSource.SLOTS,
            null,
            null,
            SlotCoverageStrategy.GREEDY
        )
    }

    @Test
    fun `orchestration marks run running before releasing startup lock`() {
        val persistenceService = mock(ComplexPlanRunPersistenceService::class.java)
        val queryService = mock(ComplexPlanRunQueryService::class.java)
        val calculationService = mock(ComplexPlanCalculationService::class.java)
        val satelliteService = mock(SatelliteService::class.java)
        val lockService = mock(ComplexPlanExecutionLockService::class.java)
        val lockHandle = mock(ComplexPlanExecutionLockService.ExecutionLockHandle::class.java)

        val orchestrationService = ComplexPlanRunOrchestrationService(
            complexPlanRunPersistenceService = persistenceService,
            complexPlanRunQueryService = queryService,
            complexPlanCalculationService = calculationService,
            satelliteService = satelliteService,
            complexPlanExecutionLockService = lockService
        )

        val request = ComplexPlanProcessRequestDTO(
            intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
            intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
            satelliteId = 22L
        )
        val createdRun = ComplexPlanRunEntity(
            id = 10L,
            status = ComplexPlanRunStatus.CREATED,
            intervalStart = request.intervalStart,
            intervalEnd = request.intervalEnd
        )
        val runningRun = ComplexPlanRunEntity(
            id = 10L,
            status = ComplexPlanRunStatus.RUNNING,
            intervalStart = request.intervalStart,
            intervalEnd = request.intervalEnd
        )
        val response = ComplexPlanRunResponseDTO(
            runId = 10L,
            status = ComplexPlanRunStatus.COMPLETED,
            intervalStart = request.intervalStart,
            intervalEnd = request.intervalEnd,
            satelliteIds = listOf(22L),
            requestedBy = "tester",
            createdAt = request.intervalStart.minusMinutes(1),
            startedAt = request.intervalStart,
            finishedAt = request.intervalStart.plusMinutes(1),
            durationMs = 60_000,
            errorMessage = null,
            calculatedModesCount = 1,
            bookedModesCount = 0,
            complanModesCount = 1,
            mixedModesCount = 0,
            affectedSatellitesCount = 1,
            processedCellsCount = 1,
            bookedSlotLinksCount = 0,
            snapshotIds = listOf(501L),
            requestPayload = "{}"
        )

        doReturn(createdRun)
            .`when`(persistenceService)
            .createRun(
                ComplexPlanRunCreateDTO(
                    intervalStart = request.intervalStart,
                    intervalEnd = request.intervalEnd,
                    satelliteIds = listOf(22L),
                    requestedBy = "tester",
                    requestPayload = "{\"intervalStart\":\"2026-03-26T10:00:00\",\"intervalEnd\":\"2026-03-26T12:00:00\",\"satelliteId\":22,\"satelliteIds\":[],\"sun\":null,\"coverageSource\":null,\"countLat\":null,\"countLong\":null,\"coverageStrategy\":\"GREEDY\"}"
                )
            )
        doReturn(lockHandle).`when`(lockService).tryAcquire(listOf(22L))
        doReturn(emptyList<Long>())
            .`when`(persistenceService)
            .findOverlappingRunningRunIds(listOf(22L), request.intervalStart, request.intervalEnd, 10L)
        doReturn(runningRun).`when`(persistenceService).markRunning(10L)
        doReturn(ComplexPlanCalculationResult(calculatedModesCount = 1))
            .`when`(calculationService)
            .process(
                10L,
                request.intervalStart,
                request.intervalEnd,
                listOf(22L),
                null,
                CoverageCalculationSource.SLOTS,
                null,
                null,
                SlotCoverageStrategy.GREEDY
            )
        doReturn(createdRun)
            .`when`(persistenceService)
            .markCompleted(10L, ComplexPlanRunStatisticsDTO(calculatedModesCount = 1))
        doReturn(response).`when`(queryService).getRun(10L)

        val result = orchestrationService.process(request, requestedBy = "tester")

        val ordered = inOrder(persistenceService, lockHandle, calculationService)
        ordered.verify(persistenceService)
            .findOverlappingRunningRunIds(listOf(22L), request.intervalStart, request.intervalEnd, 10L)
        ordered.verify(persistenceService).markRunning(10L)
        ordered.verify(lockHandle).close()
        ordered.verify(calculationService).process(
            10L,
            request.intervalStart,
            request.intervalEnd,
            listOf(22L),
            null,
            CoverageCalculationSource.SLOTS,
            null,
            null,
            SlotCoverageStrategy.GREEDY
        )
        assertEquals(10L, result.runId)
    }

    @Test
    fun `orchestration passes requested coverage source to calculation`() {
        val persistenceService = mock(ComplexPlanRunPersistenceService::class.java)
        val queryService = mock(ComplexPlanRunQueryService::class.java)
        val calculationService = mock(ComplexPlanCalculationService::class.java)
        val satelliteService = mock(SatelliteService::class.java)
        val lockService = mock(ComplexPlanExecutionLockService::class.java)
        val lockHandle = mock(ComplexPlanExecutionLockService.ExecutionLockHandle::class.java)

        val orchestrationService = ComplexPlanRunOrchestrationService(
            complexPlanRunPersistenceService = persistenceService,
            complexPlanRunQueryService = queryService,
            complexPlanCalculationService = calculationService,
            satelliteService = satelliteService,
            complexPlanExecutionLockService = lockService
        )

        val request = ComplexPlanProcessRequestDTO(
            intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
            intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
            satelliteId = 22L,
            coverageSource = CoverageCalculationSource.COVERAGE_SCHEME,
            countLat = 2,
            countLong = 3,
            coverageStrategy = SlotCoverageStrategy.CONTINUOUS
        )
        val createdRun = ComplexPlanRunEntity(
            id = 10L,
            status = ComplexPlanRunStatus.CREATED,
            intervalStart = request.intervalStart,
            intervalEnd = request.intervalEnd
        )
        val response = ComplexPlanRunResponseDTO(
            runId = 10L,
            status = ComplexPlanRunStatus.COMPLETED,
            intervalStart = request.intervalStart,
            intervalEnd = request.intervalEnd,
            satelliteIds = listOf(22L),
            requestedBy = "tester",
            createdAt = request.intervalStart.minusMinutes(1),
            startedAt = request.intervalStart,
            finishedAt = request.intervalStart.plusMinutes(1),
            durationMs = 60_000,
            errorMessage = null,
            calculatedModesCount = 0,
            bookedModesCount = 0,
            complanModesCount = 0,
            mixedModesCount = 0,
            affectedSatellitesCount = 0,
            processedCellsCount = 0,
            bookedSlotLinksCount = 0,
            snapshotIds = emptyList(),
            requestPayload = "{}"
        )

        doReturn(createdRun).`when`(persistenceService).createRun(
            ComplexPlanRunCreateDTO(
                intervalStart = request.intervalStart,
                intervalEnd = request.intervalEnd,
                satelliteIds = listOf(22L),
                requestedBy = "tester",
                requestPayload = "{\"intervalStart\":\"2026-03-26T10:00:00\",\"intervalEnd\":\"2026-03-26T12:00:00\",\"satelliteId\":22,\"satelliteIds\":[],\"sun\":null,\"coverageSource\":\"COVERAGE_SCHEME\",\"countLat\":2,\"countLong\":3,\"coverageStrategy\":\"CONTINUOUS\"}"
            )
        )
        doReturn(lockHandle).`when`(lockService).tryAcquire(listOf(22L))
        doReturn(emptyList<Long>())
            .`when`(persistenceService)
            .findOverlappingRunningRunIds(listOf(22L), request.intervalStart, request.intervalEnd, 10L)
        doReturn(createdRun).`when`(persistenceService).markRunning(10L)
        doReturn(ComplexPlanCalculationResult()).`when`(calculationService).process(
            10L,
            request.intervalStart,
            request.intervalEnd,
            listOf(22L),
            null,
            CoverageCalculationSource.COVERAGE_SCHEME,
            2,
            3,
            SlotCoverageStrategy.CONTINUOUS
        )
        doReturn(createdRun).`when`(persistenceService).markCompleted(10L, ComplexPlanRunStatisticsDTO())
        doReturn(response).`when`(queryService).getRun(10L)

        orchestrationService.process(request, requestedBy = "tester")

        verify(calculationService).process(
            10L,
            request.intervalStart,
            request.intervalEnd,
            listOf(22L),
            null,
            CoverageCalculationSource.COVERAGE_SCHEME,
            2,
            3,
            SlotCoverageStrategy.CONTINUOUS
        )
    }
}
