package space.nstart.pcp_tgu_service.service

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
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 space.nstart.pcp_tgu_service.config.PlanningProperties
import space.nstart.pcp_tgu_service.domain.CalculatedPlan
import space.nstart.pcp_tgu_service.dto.ProcessStartResponse
import space.nstart.pcp_tgu_service.entity.PlanIssueAttemptEntity
import space.nstart.pcp_tgu_service.entity.PlannedPlanEntity
import space.nstart.pcp_tgu_service.entity.PlannedPlanStatus
import space.nstart.pcp_tgu_service.entity.SpacecraftPlanningStateEntity
import space.nstart.pcp_tgu_service.entity.SpacecraftSnapshotEntity
import space.nstart.pcp_tgu_service.repository.PlanIssueAttemptRepository
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import java.time.LocalDateTime
import java.util.Optional
import java.util.UUID

class IssueDuePlanWorkerOnDemandTest {

    private val spacecraftId = "25544"

    @Test
    fun `materializes due projection head as ISSUING and starts one attempt`() {
        val planStore = linkedMapOf<UUID, PlannedPlanEntity>()
        val attemptStore = linkedMapOf<UUID, PlanIssueAttemptEntity>()
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId)
        // issueNotBefore = start - 30m; start=now+10m → issueNotBefore in the past → due now.
        val head = plan(now().plusMinutes(10), now().plusHours(3))
        val worker = worker(planStore, attemptStore, state, projection = listOf(head))

        worker.issueDuePlans()

        assertEquals(1, planStore.size)
        val materialized = planStore.values.single()
        assertEquals(head.startTime, materialized.startTime)
        // markStarted перевёл выданный план в WAITING_DECISION.
        assertEquals(PlannedPlanStatus.WAITING_DECISION, materialized.status)
        assertNotNull(state.activeAttemptId)
        assertEquals(1, attemptStore.size)
    }

    @Test
    fun `does not issue when projection head is not due yet`() {
        val planStore = linkedMapOf<UUID, PlannedPlanEntity>()
        val attemptStore = linkedMapOf<UUID, PlanIssueAttemptEntity>()
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId)
        // start far in the future → issueNotBefore well after now → not due.
        val head = plan(now().plusHours(10), now().plusHours(13))
        val worker = worker(planStore, attemptStore, state, projection = listOf(head))

        worker.issueDuePlans()

        assertTrue(planStore.isEmpty())
        assertNull(state.activeAttemptId)
    }

    @Test
    fun `urgent reissue issues nearest window past the floor immediately and clears the flag`() {
        val planStore = linkedMapOf<UUID, PlannedPlanEntity>()
        val attemptStore = linkedMapOf<UUID, PlanIssueAttemptEntity>()
        val floor = now().plusMinutes(30)
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, urgentReissueFrom = floor)
        val beforeFloor = plan(now().plusMinutes(10), now().plusHours(3))
        val afterFloor = plan(now().plusMinutes(40), now().plusHours(4))
        val worker = worker(planStore, attemptStore, state, projection = listOf(beforeFloor, afterFloor))

        worker.issueDuePlans()

        val materialized = planStore.values.single()
        assertEquals(afterFloor.startTime, materialized.startTime)
        assertNull(state.urgentReissueFrom)
        assertNotNull(state.activeAttemptId)
    }

    @Test
    fun `skips spacecraft that already has an active attempt`() {
        val planStore = linkedMapOf<UUID, PlannedPlanEntity>()
        val attemptStore = linkedMapOf<UUID, PlanIssueAttemptEntity>()
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, activeAttemptId = UUID.randomUUID())
        val head = plan(now().plusMinutes(10), now().plusHours(3))
        val projection = mock(PlanProjectionService::class.java)
        `when`(projection.project(spacecraftId)).thenReturn(listOf(head))
        val worker = worker(planStore, attemptStore, state, projectionService = projection)

        worker.issueDuePlans()

        assertTrue(planStore.isEmpty())
        verify(projection, never()).project(spacecraftId)
    }

    @Test
    fun `advances chain past WAITING_DECISION plan once grace elapsed and issues next without accepting it`() {
        val planStore = linkedMapOf<UUID, PlannedPlanEntity>()
        val attemptStore = linkedMapOf<UUID, PlanIssueAttemptEntity>()
        // Активный план висит в WAITING_DECISION; старт был 40 минут назад (grace=30 → истёк).
        val unconfirmed = persistedPlan(
            status = PlannedPlanStatus.WAITING_DECISION,
            start = now().minusMinutes(40),
            end = now().plusHours(1)
        )
        planStore[unconfirmed.planId] = unconfirmed
        val state = SpacecraftPlanningStateEntity(
            spacecraftId = spacecraftId,
            currentBasePlanId = UUID.randomUUID(),
            activePlanId = unconfirmed.planId,
            activeAttemptId = UUID.randomUUID()
        )
        val next = plan(now().plusMinutes(10), now().plusHours(3))
        val worker = worker(planStore, attemptStore, state, projection = listOf(next))

        worker.issueDuePlans()

        // Незакрытый план НЕ помечается ACCEPTED — остаётся WAITING_DECISION.
        assertEquals(PlannedPlanStatus.WAITING_DECISION, planStore[unconfirmed.planId]!!.status)
        // Якорь цепочки сдвинут на него, следующий план выдан с новым attempt.
        assertEquals(unconfirmed.planId, state.currentBasePlanId)
        assertEquals(2, planStore.size)
        assertNotNull(state.activeAttemptId)
        assertEquals(next.startTime, planStore.values.single { it.planId != unconfirmed.planId }.startTime)
    }

    @Test
    fun `keeps blocking on WAITING_DECISION plan before grace elapses`() {
        val planStore = linkedMapOf<UUID, PlannedPlanEntity>()
        val attemptStore = linkedMapOf<UUID, PlanIssueAttemptEntity>()
        // Старт был всего 10 минут назад (grace=30 → ещё не истёк) → блокировка держится.
        val unconfirmed = persistedPlan(
            status = PlannedPlanStatus.WAITING_DECISION,
            start = now().minusMinutes(10),
            end = now().plusHours(1)
        )
        planStore[unconfirmed.planId] = unconfirmed
        val attemptId = UUID.randomUUID()
        val state = SpacecraftPlanningStateEntity(
            spacecraftId = spacecraftId,
            activePlanId = unconfirmed.planId,
            activeAttemptId = attemptId
        )
        val worker = worker(planStore, attemptStore, state, projection = listOf(plan(now().plusMinutes(10), now().plusHours(3))))

        worker.issueDuePlans()

        assertEquals(1, planStore.size)
        assertEquals(attemptId, state.activeAttemptId)
    }

    private fun persistedPlan(status: PlannedPlanStatus, start: LocalDateTime, end: LocalDateTime): PlannedPlanEntity =
        PlannedPlanEntity(
            planId = UUID.randomUUID(),
            spacecraftId = spacecraftId,
            startTime = start,
            endTime = end,
            kppId = "KPP-1",
            status = status
        )

    private fun worker(
        planStore: MutableMap<UUID, PlannedPlanEntity>,
        attemptStore: MutableMap<UUID, PlanIssueAttemptEntity>,
        state: SpacecraftPlanningStateEntity,
        projection: List<CalculatedPlan> = emptyList(),
        projectionService: PlanProjectionService = projectionReturning(projection)
    ): IssueDuePlanWorker {
        val snapshotRepository = mock(SpacecraftSnapshotRepository::class.java)
        `when`(snapshotRepository.findAllByActiveTrue()).thenReturn(
            listOf(SpacecraftSnapshotEntity(spacecraftId = spacecraftId, noradId = 25544L))
        )
        val starter = mock(BpmnPlanIssueProcessStarter::class.java)
        `when`(starter.startProcess(anyNonNull(), anyNonNull())).thenAnswer { invocation ->
            val p = invocation.arguments[0] as PlannedPlanEntity
            val a = invocation.arguments[1] as PlanIssueAttemptEntity
            ProcessStartResponse(100L + a.attemptNo, 200L, "createSatelliteMission", 1, p.planId, a.attemptId)
        }
        return IssueDuePlanWorker(
            planningProperties = planningProperties(),
            spacecraftSnapshotRepository = snapshotRepository,
            plannedPlanRepository = plannedRepository(planStore),
            planIssueAttemptRepository = attemptRepository(attemptStore),
            spacecraftPlanningStateJpaRepository = stateRepository(state),
            planProjectionService = projectionService,
            bpmnPlanIssueProcessStarter = starter,
            transactionTemplate = transactionTemplate()
        )
    }

    private fun projectionReturning(projection: List<CalculatedPlan>): PlanProjectionService {
        val service = mock(PlanProjectionService::class.java)
        `when`(service.project(spacecraftId)).thenReturn(projection)
        return service
    }

    private fun plannedRepository(store: MutableMap<UUID, PlannedPlanEntity>): PlannedPlanRepository {
        val repository = mock(PlannedPlanRepository::class.java)
        `when`(repository.findById(anyNonNull())).thenAnswer { invocation -> Optional.ofNullable(store[invocation.arguments[0]]) }
        `when`(repository.save(anyNonNull())).thenAnswer { invocation ->
            val plan = invocation.arguments[0] as PlannedPlanEntity
            store[plan.planId] = plan
            plan
        }
        `when`(
            repository.insertIssuingPlan(
                anyNonNull(), anyNonNull(), anyNonNull(), anyNonNull(), anyNonNull(),
                anyNonNull(), anyOrNull(), org.mockito.ArgumentMatchers.anyLong(), anyNonNull()
            )
        ).thenAnswer { invocation ->
            val entity = PlannedPlanEntity(
                planId = invocation.arguments[0] as UUID,
                spacecraftId = invocation.arguments[1] as String,
                startTime = invocation.arguments[2] as LocalDateTime,
                endTime = invocation.arguments[3] as LocalDateTime,
                kppId = invocation.arguments[4] as String,
                status = PlannedPlanStatus.ISSUING,
                issueNotBefore = invocation.arguments[5] as LocalDateTime
            )
            store[entity.planId] = entity
            entity
        }
        return repository
    }

    private fun attemptRepository(store: MutableMap<UUID, PlanIssueAttemptEntity>): PlanIssueAttemptRepository {
        val repository = mock(PlanIssueAttemptRepository::class.java)
        `when`(repository.findById(anyNonNull())).thenAnswer { invocation -> Optional.ofNullable(store[invocation.arguments[0]]) }
        `when`(repository.findTopByPlanIdOrderByAttemptNoDesc(anyNonNull())).thenAnswer { invocation ->
            store.values.filter { attempt -> attempt.planId == invocation.arguments[0] }.maxByOrNull { attempt -> attempt.attemptNo }
        }
        `when`(repository.save(anyNonNull())).thenAnswer { invocation ->
            val attempt = invocation.arguments[0] as PlanIssueAttemptEntity
            store[attempt.attemptId] = attempt
            attempt
        }
        return repository
    }

    private fun stateRepository(state: SpacecraftPlanningStateEntity): SpacecraftPlanningStateJpaRepository {
        val repository = mock(SpacecraftPlanningStateJpaRepository::class.java)
        `when`(repository.findById(state.spacecraftId)).thenReturn(Optional.of(state))
        `when`(repository.lockBySpacecraftId(state.spacecraftId)).thenReturn(state)
        `when`(repository.save(anyNonNull())).thenAnswer { invocation -> invocation.arguments[0] }
        return repository
    }

    private fun plan(start: LocalDateTime, end: LocalDateTime): CalculatedPlan =
        CalculatedPlan(spacecraftId = spacecraftId, startTime = start, endTime = end, kppId = "KPP-1")

    private fun now(): LocalDateTime = LocalDateTime.now()

    private fun planningProperties(): PlanningProperties =
        PlanningProperties(30, 180, 60, false, 10, 14, 0, 30_000, 5, 30, 300, 30)

    private fun transactionTemplate(): TransactionTemplate =
        TransactionTemplate(
            object : PlatformTransactionManager {
                override fun getTransaction(definition: TransactionDefinition?): TransactionStatus = SimpleTransactionStatus()
                override fun commit(status: TransactionStatus) = Unit
                override fun rollback(status: TransactionStatus) = Unit
            }
        )

    @Suppress("UNCHECKED_CAST")
    private fun <T> anyNonNull(): T {
        org.mockito.ArgumentMatchers.any<T>()
        return null as T
    }

    @Suppress("UNCHECKED_CAST")
    private fun <T> anyOrNull(): T {
        org.mockito.ArgumentMatchers.any<T>()
        return null as T
    }
}
