package space.nstart.pcp.complan.service

import com.fasterxml.jackson.databind.ObjectMapper
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.TransactionDefinition
import org.springframework.transaction.TransactionStatus
import org.springframework.transaction.support.SimpleTransactionStatus
import org.springframework.transaction.support.TransactionTemplate
import org.springframework.web.server.ResponseStatusException
import space.nstart.pcp.complan.apl.BasicPlanCalculator
import space.nstart.pcp.complan.db.entity.DynamicPlanIntervalEntity
import space.nstart.pcp.complan.db.entity.DynamicPlanRouteEntity
import space.nstart.pcp.complan.db.entity.DynamicPlanRunEntity
import space.nstart.pcp.complan.db.repository.DynamicPlanIntervalRepository
import space.nstart.pcp.complan.db.repository.DynamicPlanRouteRepository
import space.nstart.pcp.complan.db.repository.DynamicPlanRunRepository
import space.nstart.pcp.complan.types.ComplexPlanCalculationOutput
import space.nstart.pcp.complan.types.ComplexPlanCalculationRequestDTO
import space.nstart.pcp.complan.types.ComplexPlanCalculationResultDTO
import space.nstart.pcp.complan.types.ComplexPlanCalculationStatus
import space.nstart.pcp.complan.types.DynamicPlanCalculationMode
import space.nstart.pcp.complan.types.DynamicPlanRevolutionMode
import space.nstart.pcp.complan.types.DynamicPlanRunStatus
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.Optional
import java.util.UUID
import java.util.concurrent.AbstractExecutorService
import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertNull

class DynamicPlanRunServiceTest {

    private val requestId = UUID.fromString("11111111-1111-4111-8111-111111111111")
    private val request = ComplexPlanCalculationRequestDTO(
        requestId = requestId,
        satelliteIds = listOf(101L, 202L),
        calculationStart = LocalDateTime.of(2026, 4, 8, 10, 0),
        calculationEnd = LocalDateTime.of(2026, 4, 8, 12, 0)
    )

    @Test
    fun `startCalculation creates pending run and queues background work`() {
        val fixture = fixture()

        val run = fixture.service.startCalculation(request)

        assertEquals(DynamicPlanRunStatus.PENDING, run.status)
        assertEquals(requestId, run.requestId)
        assertEquals(listOf(101L, 202L), run.satelliteIds)
        assertEquals(DynamicPlanCalculationMode.FULL, run.calculationMode)
        assertEquals(DynamicPlanRevolutionMode.BOTH, run.revolutionMode)
        assertEquals(false, run.filterCoveredRoutes)
        assertEquals(1, fixture.executor.queuedTasksCount())
        assertNotNull(fixture.store[run.runId])
    }

    @Test
    fun `queued calculation stores completed result`() {
        val fixture = fixture()
        val result = ComplexPlanCalculationResultDTO(
            status = ComplexPlanCalculationStatus.COMPLETED,
            requestId = requestId,
            satelliteIds = listOf(101L, 202L),
            missingSatelliteIds = emptyList(),
            calculationStart = request.calculationStart,
            calculationEnd = request.calculationEnd,
            routesCount = 1,
            durationMs = 5000
        )
        val route = SurveyContour(
            mpl = SquareViewParamDTO(noradId = 101L),
            time = LocalDateTime.of(2026, 4, 8, 10, 10),
            duration = 30.0,
            rev = 55L,
            gamma = 11.5,
            geometry = "POLYGON ((0 0, 0 1, 1 1, 0 0))"
        )
        val interval = IntervalItemDTO(
            id = UUID.fromString("33333333-3333-4333-8333-333333333333"),
            regionId = UUID.fromString("44444444-4444-4444-8444-444444444444"),
            revSign = RevolutionSign.ASC,
            geometry = "POLYGON ((0 0, 0 2, 2 2, 0 0))",
            mpl = mutableListOf(SquareViewParamDTO(noradId = 101L))
        )
        doReturn(ComplexPlanCalculationOutput(result = result, routes = listOf(route), intervals = listOf(interval)))
            .`when`(fixture.basePlan).calculate(request)

        val run = fixture.service.startCalculation(request)
        fixture.executor.runNext()

        val storedRun = fixture.service.getRun(run.runId)
        val storedResult = fixture.service.getResult(run.runId)
        val storedRoutes = fixture.service.getRoutes(run.runId, limit = 1000, offset = 0)
        val storedIntervals = fixture.service.getIntervals(run.runId, limit = 1000, offset = 0)

        assertEquals(DynamicPlanRunStatus.COMPLETED, storedRun.status)
        assertEquals("COMPLETED", storedRun.resultStatus)
        assertEquals(1, storedRun.routesCount)
        assertEquals(5000, storedRun.durationMs)
        assertNull(storedRun.errorMessage)
        assertEquals(result, storedResult)
        assertEquals(1, storedRoutes.size)
        assertEquals(101L, storedRoutes[0].satelliteId)
        assertEquals("POLYGON ((0 0, 0 1, 1 1, 0 0))", storedRoutes[0].contourWkt)
        assertEquals(1, storedIntervals.size)
        assertEquals(interval.id, storedIntervals[0].intervalId)
        assertEquals(RevolutionSign.ASC, storedIntervals[0].revSign)
        assertEquals(1, storedIntervals[0].observationParametersCount)
        verify(fixture.aplSettingsLoader).loadLatestSettings()
        verify(fixture.basePlan).calculate(request)
    }

    @Test
    fun `queued calculation keeps selected calculation mode`() {
        val fixture = fixture()
        val greedyRequest = request.copy(
            calculationMode = DynamicPlanCalculationMode.GREEDY,
            revolutionMode = DynamicPlanRevolutionMode.DESC,
            filterCoveredRoutes = true
        )
        val result = ComplexPlanCalculationResultDTO(
            status = ComplexPlanCalculationStatus.COMPLETED,
            requestId = requestId,
            satelliteIds = listOf(101L, 202L),
            missingSatelliteIds = emptyList(),
            calculationStart = request.calculationStart,
            calculationEnd = request.calculationEnd,
            calculationMode = DynamicPlanCalculationMode.GREEDY,
            revolutionMode = DynamicPlanRevolutionMode.DESC,
            filterCoveredRoutes = true,
            routesCount = 0,
            durationMs = 1000
        )
        doReturn(ComplexPlanCalculationOutput(result = result, routes = emptyList())).`when`(fixture.basePlan).calculate(greedyRequest)

        val run = fixture.service.startCalculation(greedyRequest)
        fixture.executor.runNext()

        val storedRun = fixture.service.getRun(run.runId)
        val storedResult = fixture.service.getResult(run.runId)

        assertEquals(DynamicPlanCalculationMode.GREEDY, storedRun.calculationMode)
        assertEquals(DynamicPlanRevolutionMode.DESC, storedRun.revolutionMode)
        assertEquals(true, storedRun.filterCoveredRoutes)
        assertEquals(DynamicPlanCalculationMode.GREEDY, storedResult.calculationMode)
        assertEquals(DynamicPlanRevolutionMode.DESC, storedResult.revolutionMode)
        assertEquals(true, storedResult.filterCoveredRoutes)
        verify(fixture.basePlan).calculate(greedyRequest)
    }

    @Test
    fun `getResult reads legacy result json without calculation mode`() {
        val fixture = fixture()
        val runId = UUID.fromString("55555555-5555-4555-8555-555555555555")
        fixture.store[runId] = runEntity(
            id = runId,
            status = DynamicPlanRunStatus.COMPLETED
        ).apply {
            resultJson = """
                {
                  "status": "COMPLETED",
                  "requestId": "$requestId",
                  "satelliteIds": [101, 202],
                  "missingSatelliteIds": [],
                  "calculationStart": "2026-04-08T10:00:00",
                  "calculationEnd": "2026-04-08T12:00:00",
                  "routesCount": 1,
                  "durationMs": 5000
                }
            """.trimIndent()
        }

        val result = fixture.service.getResult(runId)

        assertEquals(DynamicPlanCalculationMode.FULL, result.calculationMode)
        assertEquals(DynamicPlanRevolutionMode.BOTH, result.revolutionMode)
        assertEquals(false, result.filterCoveredRoutes)
        assertEquals(1, result.routesCount)
    }

    @Test
    fun `getResult rejects unfinished run`() {
        val fixture = fixture()
        val run = fixture.service.startCalculation(request)

        assertFailsWith<ResponseStatusException> {
            fixture.service.getResult(run.runId)
        }
    }

    @Test
    fun `listRuns returns latest runs and supports status filter`() {
        val fixture = fixture()
        val olderRun = runEntity(
            id = UUID.fromString("22222222-2222-4222-8222-222222222222"),
            status = DynamicPlanRunStatus.COMPLETED,
            createdAt = LocalDateTime.of(2026, 4, 8, 9, 0)
        )
        val newerRun = runEntity(
            id = UUID.fromString("33333333-3333-4333-8333-333333333333"),
            status = DynamicPlanRunStatus.FAILED,
            createdAt = LocalDateTime.of(2026, 4, 8, 10, 0)
        )
        fixture.store[olderRun.id!!] = olderRun
        fixture.store[newerRun.id!!] = newerRun

        val allRuns = fixture.service.listRuns(status = null, limit = 10, offset = 0)
        val failedRuns = fixture.service.listRuns(status = DynamicPlanRunStatus.FAILED, limit = 10, offset = 0)

        assertEquals(listOf(newerRun.id, olderRun.id), allRuns.map { run -> run.runId })
        assertEquals(listOf(newerRun.id), failedRuns.map { run -> run.runId })
    }

    @Test
    fun `recoverInterruptedRuns marks pending and running runs as failed`() {
        val fixture = fixture()
        val pendingRun = runEntity(
            id = UUID.fromString("22222222-2222-4222-8222-222222222222"),
            status = DynamicPlanRunStatus.PENDING
        )
        val runningRun = runEntity(
            id = UUID.fromString("33333333-3333-4333-8333-333333333333"),
            status = DynamicPlanRunStatus.RUNNING
        )
        val completedRun = runEntity(
            id = UUID.fromString("44444444-4444-4444-8444-444444444444"),
            status = DynamicPlanRunStatus.COMPLETED
        )
        fixture.store[pendingRun.id!!] = pendingRun
        fixture.store[runningRun.id!!] = runningRun
        fixture.store[completedRun.id!!] = completedRun

        fixture.service.recoverInterruptedRuns()

        assertEquals(DynamicPlanRunStatus.FAILED, pendingRun.status)
        assertEquals(DynamicPlanRunStatus.FAILED, runningRun.status)
        assertEquals(DynamicPlanRunStatus.COMPLETED, completedRun.status)
        assertEquals("Calculation was interrupted by service restart", pendingRun.errorMessage)
        assertEquals("Calculation was interrupted by service restart", runningRun.errorMessage)
        assertNotNull(pendingRun.finishedAt)
        assertNotNull(runningRun.finishedAt)
        assertNull(completedRun.finishedAt)
    }

    private fun fixture(): Fixture {
        val store = linkedMapOf<UUID, DynamicPlanRunEntity>()
        val routeStore = mutableListOf<DynamicPlanRouteEntity>()
        val intervalStore = mutableListOf<DynamicPlanIntervalEntity>()
        val repository = mock(DynamicPlanRunRepository::class.java)
        val routeRepository = mock(DynamicPlanRouteRepository::class.java)
        val intervalRepository = mock(DynamicPlanIntervalRepository::class.java)
        org.mockito.Mockito.`when`(
            repository.save(ArgumentMatchers.any(DynamicPlanRunEntity::class.java) ?: DynamicPlanRunEntity(id = UUID.randomUUID()))
        ).thenAnswer { invocation ->
            val entity = invocation.arguments[0] as DynamicPlanRunEntity
            store[entity.id!!] = entity
            entity
        }
        org.mockito.Mockito.`when`(
            repository.findById(ArgumentMatchers.any(UUID::class.java) ?: UUID.randomUUID())
        ).thenAnswer { invocation ->
            Optional.ofNullable(store[invocation.arguments[0] as UUID])
        }
        org.mockito.Mockito.`when`(
            repository.saveAll(ArgumentMatchers.anyIterable<DynamicPlanRunEntity>() ?: emptyList())
        ).thenAnswer { invocation ->
            val entities = invocation.arguments[0] as Iterable<DynamicPlanRunEntity>
            entities.map { entity ->
                store[entity.id!!] = entity
                entity
            }
        }
        org.mockito.Mockito.`when`(
            repository.findRuns(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())
        ).thenAnswer { invocation ->
            val limit = invocation.arguments[0] as Int
            val offset = invocation.arguments[1] as Int
            store.values
                .sortedByDescending { run -> run.createdAt }
                .drop(offset)
                .take(limit)
        }
        org.mockito.Mockito.`when`(
            repository.findRunsByStatus(
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyInt(),
                ArgumentMatchers.anyInt()
            )
        ).thenAnswer { invocation ->
            val status = DynamicPlanRunStatus.valueOf(invocation.arguments[0] as String)
            val limit = invocation.arguments[1] as Int
            val offset = invocation.arguments[2] as Int
            store.values
                .filter { run -> run.status == status }
                .sortedByDescending { run -> run.createdAt }
                .drop(offset)
                .take(limit)
        }
        org.mockito.Mockito.`when`(
            repository.findAllByStatusIn(ArgumentMatchers.anyCollection<DynamicPlanRunStatus>() ?: emptyList())
        ).thenAnswer { invocation ->
            val statuses = invocation.arguments[0] as Collection<DynamicPlanRunStatus>
            store.values.filter { run -> run.status in statuses }
        }
        org.mockito.Mockito.`when`(
            routeRepository.saveAll(ArgumentMatchers.anyList<DynamicPlanRouteEntity>() ?: emptyList())
        ).thenAnswer { invocation ->
            val entities = invocation.arguments[0] as Iterable<DynamicPlanRouteEntity>
            entities.map { entity ->
                entity.id = (routeStore.size + 1).toLong()
                routeStore.add(entity)
                entity
            }
        }
        org.mockito.Mockito.`when`(
            intervalRepository.saveAll(ArgumentMatchers.anyList<DynamicPlanIntervalEntity>() ?: emptyList())
        ).thenAnswer { invocation ->
            val entities = invocation.arguments[0] as Iterable<DynamicPlanIntervalEntity>
            entities.map { entity ->
                entity.id = (intervalStore.size + 1).toLong()
                intervalStore.add(entity)
                entity
            }
        }
        org.mockito.Mockito.`when`(
            routeRepository.findRoutesByRunId(
                ArgumentMatchers.any(UUID::class.java) ?: UUID.randomUUID(),
                ArgumentMatchers.anyInt(),
                ArgumentMatchers.anyInt()
            )
        )
            .thenAnswer { invocation ->
                val runId = invocation.arguments[0] as UUID
                routeStore.filter { route -> route.run?.id == runId }.sortedWith(compareBy({ it.startTime }, { it.id }))
            }
        org.mockito.Mockito.`when`(
            intervalRepository.findIntervalsByRunId(
                ArgumentMatchers.any(UUID::class.java) ?: UUID.randomUUID(),
                ArgumentMatchers.anyInt(),
                ArgumentMatchers.anyInt()
            )
        )
            .thenAnswer { invocation ->
                val runId = invocation.arguments[0] as UUID
                intervalStore.filter { interval -> interval.run?.id == runId }.sortedWith(compareBy({ it.intervalOrder }, { it.id }))
            }

        val basePlan = mock(BasicPlanCalculator::class.java)
        val aplSettingsLoader = mock(AplSettingsLoader::class.java)
        val executor = QueueingExecutorService()
        val service = DynamicPlanRunService(
            runRepository = repository,
            routeRepository = routeRepository,
            intervalRepository = intervalRepository,
            basePlan = basePlan,
            aplSettingsLoader = aplSettingsLoader,
            objectMapper = ObjectMapper().findAndRegisterModules(),
            transactionTemplate = TransactionTemplate(NoopTransactionManager()),
            dynamicPlanExecutor = executor
        )

        return Fixture(service, basePlan, aplSettingsLoader, executor, store)
    }

    private fun runEntity(
        id: UUID,
        status: DynamicPlanRunStatus,
        createdAt: LocalDateTime = LocalDateTime.of(2026, 4, 8, 10, 0)
    ): DynamicPlanRunEntity =
        DynamicPlanRunEntity(
            id = id,
            status = status,
            requestId = requestId,
            satelliteIds = """[101,202]""",
            calculationStart = request.calculationStart,
            calculationEnd = request.calculationEnd,
            createdAt = createdAt
        )

    private data class Fixture(
        val service: DynamicPlanRunService,
        val basePlan: BasicPlanCalculator,
        val aplSettingsLoader: AplSettingsLoader,
        val executor: QueueingExecutorService,
        val store: MutableMap<UUID, DynamicPlanRunEntity>
    )

    private class QueueingExecutorService : AbstractExecutorService() {
        private val tasks = ArrayDeque<Runnable>()
        private var shutdown = false

        override fun execute(command: Runnable) {
            tasks.add(command)
        }

        fun queuedTasksCount(): Int = tasks.size

        fun runNext() {
            tasks.removeFirst().run()
        }

        override fun shutdown() {
            shutdown = true
        }

        override fun shutdownNow(): MutableList<Runnable> {
            shutdown = true
            val queued = tasks.toMutableList()
            tasks.clear()
            return queued
        }

        override fun isShutdown(): Boolean = shutdown

        override fun isTerminated(): Boolean = shutdown && tasks.isEmpty()

        override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean = isTerminated
    }

    private class NoopTransactionManager : PlatformTransactionManager {
        override fun getTransaction(definition: TransactionDefinition?): TransactionStatus = SimpleTransactionStatus()

        override fun commit(status: TransactionStatus) = Unit

        override fun rollback(status: TransactionStatus) = Unit
    }
}
