package space.nstart.pcp.pcp_satellites_service.service

import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
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.entity.ComplexPlanRunStatus
import space.nstart.pcp.pcp_satellites_service.entity.SatelliteModeSource
import space.nstart.pcp.pcp_satellites_service.model.mode.SurveyMode
import space.nstart.pcp.pcp_satellites_service.repository.ComplexPlanRunRepository
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeBookedSlotRepository
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeCellRepository
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeRepository
import space.nstart.pcp.pcp_satellites_service.repository.SatelliteModeSnapshotRepository
import java.time.LocalDateTime
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

@SpringBootTest
class ComplexPlanRunPersistenceServiceTest {

    @Autowired
    private lateinit var complexPlanRunPersistenceService: ComplexPlanRunPersistenceService

    @Autowired
    private lateinit var complexPlanRunRepository: ComplexPlanRunRepository

    @Autowired
    private lateinit var satelliteModeRepository: SatelliteModeRepository

    @Autowired
    private lateinit var satelliteModeBookedSlotRepository: SatelliteModeBookedSlotRepository

    @Autowired
    private lateinit var satelliteModeCellRepository: SatelliteModeCellRepository

    @Autowired
    private lateinit var satelliteModeSnapshotRepository: SatelliteModeSnapshotRepository

    @Autowired
    private lateinit var complexPlanPersistenceService: ComplexPlanPersistenceService

    @Autowired
    private lateinit var complexPlanRunQueryService: ComplexPlanRunQueryService

    @BeforeEach
    fun setUp() {
        satelliteModeBookedSlotRepository.deleteAll()
        satelliteModeCellRepository.deleteAll()
        satelliteModeRepository.deleteAll()
        satelliteModeSnapshotRepository.deleteAll()
        complexPlanRunRepository.deleteAll()
    }

    @Test
    fun `create run stores CREATED status and then moves to RUNNING`() {
        val run = complexPlanRunPersistenceService.createRun(
            ComplexPlanRunCreateDTO(
                intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
                intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
                satelliteIds = listOf(22L, 23L),
                requestedBy = "tester",
                requestPayload = """{"k":"v"}"""
            )
        )

        assertEquals(ComplexPlanRunStatus.CREATED, run.status)
        assertEquals(listOf(22L, 23L), run.satellites.map { it.satelliteId }.sorted())

        val running = complexPlanRunPersistenceService.markRunning(run.id!!)
        assertEquals(ComplexPlanRunStatus.RUNNING, running.status)
        assertNotNull(running.startedAt)
    }

    @Test
    fun `completed run stores duration and statistics`() {
        val run = complexPlanRunPersistenceService.createRun(
            ComplexPlanRunCreateDTO(
                intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
                intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
                satelliteIds = listOf(22L),
                requestedBy = "tester",
                requestPayload = "{}"
            )
        )
        complexPlanRunPersistenceService.markRunning(run.id!!)
        Thread.sleep(5)

        val completed = complexPlanRunPersistenceService.markCompleted(
            runId = run.id!!,
            statistics = ComplexPlanRunStatisticsDTO(
                calculatedModesCount = 3,
                bookedModesCount = 1,
                complanModesCount = 1,
                mixedModesCount = 1,
                affectedSatellitesCount = 1,
                processedCellsCount = 2,
                bookedSlotLinksCount = 4,
                totalRequestsArea = 10.0,
                coveredArea = 4.0,
                coveredAreaPercent = 40.0
            )
        )

        assertEquals(ComplexPlanRunStatus.COMPLETED, completed.status)
        assertNotNull(completed.finishedAt)
        assertNotNull(completed.durationMs)
        assertTrue(completed.durationMs!! >= 0)
        assertEquals(3, completed.calculatedModesCount)
        assertEquals(1, completed.bookedModesCount)
        assertEquals(1, completed.complanModesCount)
        assertEquals(1, completed.mixedModesCount)
        assertEquals(2, completed.processedCellsCount)
        assertEquals(4, completed.bookedSlotLinksCount)
        assertEquals(10.0, completed.totalRequestsArea)
        assertEquals(4.0, completed.coveredArea)
        assertEquals(40.0, completed.coveredAreaPercent)
    }

    @Test
    fun `getRun returns linked snapshot ids and active snapshot ids`() {
        val intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0)
        val intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0)
        val run = complexPlanRunPersistenceService.createRun(
            ComplexPlanRunCreateDTO(
                intervalStart = intervalStart,
                intervalEnd = intervalEnd,
                satelliteIds = listOf(22L),
                requestedBy = "tester",
                requestPayload = "{}"
            )
        )

        complexPlanPersistenceService.saveModesForInterval(
            satelliteIds = listOf(22L),
            intervalStart = intervalStart,
            intervalEnd = intervalEnd,
            surveysBySatelliteId = mapOf(
                22L to listOf(
                    SurveyMode(
                        revolution = 123,
                        time = intervalStart.plusMinutes(5),
                        timeStop = intervalStart.plusMinutes(15),
                        roll = 3.0,
                        contourWKT = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))",
                        source = SatelliteModeSource.COMPLAN
                    )
                )
            ),
            complexPlanRunId = run.id
        )

        val response = complexPlanRunQueryService.getRun(run.id!!)

        assertEquals(1, response.snapshotIds.size)
    }

    @Test
    fun `failed run stores error and keeps audit trail`() {
        val run = complexPlanRunPersistenceService.createRun(
            ComplexPlanRunCreateDTO(
                intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
                intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
                satelliteIds = listOf(22L),
                requestedBy = null,
                requestPayload = "{}"
            )
        )
        complexPlanRunPersistenceService.markRunning(run.id!!)

        val failed = complexPlanRunPersistenceService.markFailed(run.id!!, "boom")

        assertEquals(ComplexPlanRunStatus.FAILED, failed.status)
        assertEquals("boom", failed.errorMessage)
        assertNotNull(failed.finishedAt)
        assertNotNull(failed.durationMs)
    }

    @Test
    fun `shutdown marks CREATED and RUNNING runs as FAILED and leaves completed untouched`() {
        val createdRun = complexPlanRunPersistenceService.createRun(
            ComplexPlanRunCreateDTO(
                intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
                intervalEnd = LocalDateTime.of(2026, 3, 26, 12, 0),
                satelliteIds = listOf(22L),
                requestedBy = "tester",
                requestPayload = "{}"
            )
        )
        val runningRun = complexPlanRunPersistenceService.createRun(
            ComplexPlanRunCreateDTO(
                intervalStart = LocalDateTime.of(2026, 3, 26, 13, 0),
                intervalEnd = LocalDateTime.of(2026, 3, 26, 14, 0),
                satelliteIds = listOf(23L),
                requestedBy = "tester",
                requestPayload = "{}"
            )
        )
        complexPlanRunPersistenceService.markRunning(runningRun.id!!)
        val completedRun = complexPlanRunPersistenceService.createRun(
            ComplexPlanRunCreateDTO(
                intervalStart = LocalDateTime.of(2026, 3, 26, 15, 0),
                intervalEnd = LocalDateTime.of(2026, 3, 26, 16, 0),
                satelliteIds = listOf(24L),
                requestedBy = "tester",
                requestPayload = "{}"
            )
        )
        complexPlanRunPersistenceService.markRunning(completedRun.id!!)
        complexPlanRunPersistenceService.markCompleted(
            runId = completedRun.id!!,
            statistics = ComplexPlanRunStatisticsDTO(calculatedModesCount = 1)
        )

        val affectedRuns = complexPlanRunPersistenceService.markActiveRunsFailedOnShutdown()

        assertEquals(2, affectedRuns)

        val reloadedCreated = complexPlanRunRepository.findById(createdRun.id!!).orElseThrow()
        val reloadedRunning = complexPlanRunRepository.findById(runningRun.id!!).orElseThrow()
        val reloadedCompleted = complexPlanRunRepository.findById(completedRun.id!!).orElseThrow()

        assertEquals(ComplexPlanRunStatus.FAILED, reloadedCreated.status)
        assertEquals(ComplexPlanRunStatus.FAILED, reloadedRunning.status)
        assertEquals(ComplexPlanRunStatus.COMPLETED, reloadedCompleted.status)
        assertEquals(
            ComplexPlanRunPersistenceService.APPLICATION_SHUTDOWN_ERROR_MESSAGE,
            reloadedCreated.errorMessage
        )
        assertEquals(
            ComplexPlanRunPersistenceService.APPLICATION_SHUTDOWN_ERROR_MESSAGE,
            reloadedRunning.errorMessage
        )
        assertNotNull(reloadedCreated.finishedAt)
        assertNotNull(reloadedRunning.finishedAt)
        assertNotNull(reloadedRunning.durationMs)
    }

    @Test
    fun `findOverlappingRunningRunIds is interval aware`() {
        val runningRun = complexPlanRunPersistenceService.createRun(
            ComplexPlanRunCreateDTO(
                intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
                intervalEnd = LocalDateTime.of(2026, 3, 26, 11, 0),
                satelliteIds = listOf(10L),
                requestedBy = "tester",
                requestPayload = "{}"
            )
        )
        complexPlanRunPersistenceService.markRunning(runningRun.id!!)

        val nonOverlapping = complexPlanRunPersistenceService.findOverlappingRunningRunIds(
            satelliteIds = listOf(10L),
            intervalStart = LocalDateTime.of(2026, 3, 26, 12, 0),
            intervalEnd = LocalDateTime.of(2026, 3, 26, 13, 0),
            excludedRunId = -1L
        )
        val overlapping = complexPlanRunPersistenceService.findOverlappingRunningRunIds(
            satelliteIds = listOf(10L),
            intervalStart = LocalDateTime.of(2026, 3, 26, 10, 30),
            intervalEnd = LocalDateTime.of(2026, 3, 26, 11, 30),
            excludedRunId = -1L
        )
        val otherSatellite = complexPlanRunPersistenceService.findOverlappingRunningRunIds(
            satelliteIds = listOf(11L),
            intervalStart = LocalDateTime.of(2026, 3, 26, 10, 30),
            intervalEnd = LocalDateTime.of(2026, 3, 26, 11, 30),
            excludedRunId = -1L
        )

        assertTrue(nonOverlapping.isEmpty())
        assertEquals(listOf(runningRun.id!!), overlapping)
        assertTrue(otherSatellite.isEmpty())
    }

    @Test
    fun `startup recovery marks stale RUNNING runs as FAILED`() {
        val run = complexPlanRunPersistenceService.createRun(
            ComplexPlanRunCreateDTO(
                intervalStart = LocalDateTime.of(2026, 3, 26, 10, 0),
                intervalEnd = LocalDateTime.of(2026, 3, 26, 11, 0),
                satelliteIds = listOf(10L),
                requestedBy = "tester",
                requestPayload = "{}"
            )
        )
        complexPlanRunPersistenceService.markRunning(run.id!!)

        val affectedRuns = complexPlanRunPersistenceService.markRunningRunsFailedOnStartup()

        val reloaded = complexPlanRunRepository.findById(run.id!!).orElseThrow()
        assertEquals(1, affectedRuns)
        assertEquals(ComplexPlanRunStatus.FAILED, reloaded.status)
        assertEquals(ComplexPlanRunPersistenceService.STARTUP_RECOVERY_ERROR_MESSAGE, reloaded.errorMessage)
        assertNotNull(reloaded.finishedAt)
    }

    @Test
    fun `startup recovery is no-op when there are no RUNNING runs`() {
        val affectedRuns = complexPlanRunPersistenceService.markRunningRunsFailedOnStartup()
        assertEquals(0, affectedRuns)
    }

    @Test
    fun `run keeps link to created snapshots`() {
        val intervalStart = LocalDateTime.of(2026, 3, 27, 10, 0)
        val intervalEnd = LocalDateTime.of(2026, 3, 27, 12, 0)
        val run = complexPlanRunPersistenceService.createRun(
            ComplexPlanRunCreateDTO(
                intervalStart = intervalStart,
                intervalEnd = intervalEnd,
                satelliteIds = listOf(22L),
                requestedBy = "tester",
                requestPayload = "{}"
            )
        )

        complexPlanPersistenceService.replaceModesForInterval(
            satelliteIds = listOf(22L),
            intervalStart = intervalStart,
            intervalEnd = intervalEnd,
            surveysBySatelliteId = emptyMap(),
            complexPlanRunId = run.id
        )

        val reloaded = complexPlanRunPersistenceService.getRun(run.id!!)
        assertEquals(1, reloaded.snapshots.size)
        assertEquals(22L, reloaded.snapshots.single().satelliteId)
        assertTrue(reloaded.snapshots.single().isActive)
    }
}
