package space.nstart.pcp_tgu_service.service

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.assertNotEquals
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.PlanDecision
import space.nstart.pcp_tgu_service.dto.PlanDecisionMessage
import space.nstart.pcp_tgu_service.dto.ProcessStartResponse
import space.nstart.pcp_tgu_service.entity.PlanIssueAttemptEntity
import space.nstart.pcp_tgu_service.entity.PlanIssueAttemptStatus
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.repository.PlanIssueAttemptRepository
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.ProcessedPlanDecisionEventRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime
import java.util.Optional
import java.util.UUID

class PlanIssueAndDecisionFlowTest {

    @Test
    fun `issue worker creates one active attempt and repeated issue of same plan creates new attempt after state is cleared`() {
        val spacecraftId = "25544"
        val plan = plannedPlan(spacecraftId, time(12), time(13))
        val planStore = linkedMapOf(plan.planId to plan)
        val attemptStore = linkedMapOf<UUID, PlanIssueAttemptEntity>()
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId)
        val plannedRepository = plannedRepository(planStore)
        val attemptRepository = attemptRepository(attemptStore)
        val stateRepository = stateRepository(state)
        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)
        }
        val worker = IssueDuePlanWorker(
            planningProperties = planningProperties(),
            plannedPlanRepository = plannedRepository,
            planIssueAttemptRepository = attemptRepository,
            spacecraftPlanningStateJpaRepository = stateRepository,
            bpmnPlanIssueProcessStarter = starter,
            transactionTemplate = transactionTemplate()
        )

        val firstResponse = worker.issuePlanNow(plan.planId)
        val blockedResponse = runCatching { worker.issuePlanNow(plan.planId) }.exceptionOrNull()
        state.activePlanId = null
        state.activeAttemptId = null
        plan.status = PlannedPlanStatus.PLANNED
        val secondResponse = worker.issuePlanNow(plan.planId)

        assertEquals(1, attemptStore.values.first { it.attemptId == firstResponse.attemptId }.attemptNo)
        assertEquals(2, attemptStore.values.first { it.attemptId == secondResponse.attemptId }.attemptNo)
        assertNotEquals(firstResponse.attemptId, secondResponse.attemptId)
        assertEquals(PlanIssueAttemptStatus.STARTED, attemptStore.values.first { it.attemptId == firstResponse.attemptId }.status)
        assertEquals(firstResponse.processInstanceKey, attemptStore.values.first { it.attemptId == firstResponse.attemptId }.processInstanceKey)
        assertEquals(PlannedPlanStatus.WAITING_DECISION, plan.status)
        assertTrue(blockedResponse?.message?.startsWith("404 NOT_FOUND") == true)
    }

    @Test
    fun `mark started does not roll accepted plan and attempt back after BPMN start race`() {
        val spacecraftId = "25544"
        val plan = plannedPlan(spacecraftId, time(12), time(13))
        val planStore = linkedMapOf(plan.planId to plan)
        val attemptStore = linkedMapOf<UUID, PlanIssueAttemptEntity>()
        val worker = issueWorker(planStore, attemptStore) { response ->
            attemptStore.getValue(response.attemptId).status = PlanIssueAttemptStatus.ACCEPTED
            planStore.getValue(response.planId).status = PlannedPlanStatus.ACCEPTED
        }

        val response = worker.issuePlanNow(plan.planId)
        val attempt = attemptStore.getValue(response.attemptId)

        assertEquals(PlanIssueAttemptStatus.ACCEPTED, attempt.status)
        assertEquals(PlannedPlanStatus.ACCEPTED, plan.status)
        assertEquals(response.processInstanceKey, attempt.processInstanceKey)
        assertTrue(attempt.startedAt != null)
    }

    @Test
    fun `mark started does not roll rejected planned plan and attempt back after BPMN start race`() {
        val spacecraftId = "25544"
        val plan = plannedPlan(spacecraftId, time(12), time(13))
        val planStore = linkedMapOf(plan.planId to plan)
        val attemptStore = linkedMapOf<UUID, PlanIssueAttemptEntity>()
        val worker = issueWorker(planStore, attemptStore) { response ->
            attemptStore.getValue(response.attemptId).status = PlanIssueAttemptStatus.REJECTED
            planStore.getValue(response.planId).status = PlannedPlanStatus.PLANNED
        }

        val response = worker.issuePlanNow(plan.planId)
        val attempt = attemptStore.getValue(response.attemptId)

        assertEquals(PlanIssueAttemptStatus.REJECTED, attempt.status)
        assertEquals(PlannedPlanStatus.PLANNED, plan.status)
        assertEquals(response.processInstanceKey, attempt.processInstanceKey)
        assertTrue(attempt.startedAt != null)
    }

    @Test
    fun `mark started does not roll rejected expired plan and attempt back after BPMN start race`() {
        val spacecraftId = "25544"
        val plan = plannedPlan(spacecraftId, time(12), time(13))
        val planStore = linkedMapOf(plan.planId to plan)
        val attemptStore = linkedMapOf<UUID, PlanIssueAttemptEntity>()
        val worker = issueWorker(planStore, attemptStore) { response ->
            attemptStore.getValue(response.attemptId).status = PlanIssueAttemptStatus.REJECTED
            planStore.getValue(response.planId).status = PlannedPlanStatus.EXPIRED
        }

        val response = worker.issuePlanNow(plan.planId)
        val attempt = attemptStore.getValue(response.attemptId)

        assertEquals(PlanIssueAttemptStatus.REJECTED, attempt.status)
        assertEquals(PlannedPlanStatus.EXPIRED, plan.status)
        assertEquals(response.processInstanceKey, attempt.processInstanceKey)
        assertTrue(attempt.startedAt != null)
    }

    @Test
    fun `accepted decision sets base plan clears active ids and enqueues recalculation once`() {
        val spacecraftId = "25544"
        val plan = plannedPlan(spacecraftId, time(12), time(13), PlannedPlanStatus.WAITING_DECISION)
        val attempt = attempt(plan.planId, status = PlanIssueAttemptStatus.STARTED)
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, activePlanId = plan.planId, activeAttemptId = attempt.attemptId)
        val processedRepository = mock(ProcessedPlanDecisionEventRepository::class.java)
        `when`(processedRepository.insertIfAbsent(anyNonNull(), anyNonNull())).thenReturn(1).thenReturn(0)
        val recalculationJobService = mock(PlanRecalculationJobService::class.java)
        val slotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java)
        val useCase = decisionUseCase(plan, attempt, state, processedRepository, recalculationJobService, slotsUpdateJobService)
        val message = decisionMessage(spacecraftId, plan.planId, attempt.attemptId, PlanDecision.ACCEPTED)

        useCase.handle(message)
        useCase.handle(message)

        assertEquals(PlanIssueAttemptStatus.ACCEPTED, attempt.status)
        assertEquals(PlannedPlanStatus.ACCEPTED, plan.status)
        assertEquals(plan.planId, state.currentBasePlanId)
        assertEquals(null, state.activePlanId)
        assertEquals(null, state.activeAttemptId)
        verify(recalculationJobService).enqueue(spacecraftId, "PLAN_ACCEPTED")
        verify(slotsUpdateJobService).enqueue(plan.planId)
    }

    @Test
    fun `rejected before cutoff reopens same plan without direct new attempt`() {
        val spacecraftId = "25544"
        val plan = plannedPlan(spacecraftId, LocalDateTime.now().plusHours(2), LocalDateTime.now().plusHours(3), PlannedPlanStatus.WAITING_DECISION)
        val attempt = attempt(plan.planId, status = PlanIssueAttemptStatus.STARTED)
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, activePlanId = plan.planId, activeAttemptId = attempt.attemptId)
        val recalculationJobService = mock(PlanRecalculationJobService::class.java)
        val slotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java)
        val useCase = decisionUseCase(
            plan = plan,
            attempt = attempt,
            state = state,
            recalculationJobService = recalculationJobService,
            slotsUpdateJobService = slotsUpdateJobService
        )

        useCase.handle(decisionMessage(spacecraftId, plan.planId, attempt.attemptId, PlanDecision.REJECTED))

        assertEquals(PlanIssueAttemptStatus.REJECTED, attempt.status)
        assertEquals(PlannedPlanStatus.PLANNED, plan.status)
        assertEquals(null, state.activeAttemptId)
        verify(recalculationJobService, never()).enqueue(spacecraftId, "PLAN_REJECTED_AFTER_CUTOFF")
        verify(slotsUpdateJobService, never()).enqueue(plan.planId)
    }

    @Test
    fun `rejected after cutoff expires plan and promotes next planned plan`() {
        val spacecraftId = "25544"
        val currentPlan = plannedPlan(spacecraftId, LocalDateTime.now().plusMinutes(2), LocalDateTime.now().plusMinutes(20), PlannedPlanStatus.WAITING_DECISION)
        val nextPlan = plannedPlan(spacecraftId, LocalDateTime.now().plusHours(1), LocalDateTime.now().plusHours(2), PlannedPlanStatus.PLANNED)
        val attempt = attempt(currentPlan.planId, status = PlanIssueAttemptStatus.STARTED)
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, activePlanId = currentPlan.planId, activeAttemptId = attempt.attemptId)
        val planStore = linkedMapOf(currentPlan.planId to currentPlan, nextPlan.planId to nextPlan)
        val recalculationJobService = mock(PlanRecalculationJobService::class.java)
        val useCase = decisionUseCase(
            plan = currentPlan,
            attempt = attempt,
            state = state,
            plannedRepository = plannedRepository(planStore),
            recalculationJobService = recalculationJobService
        )

        useCase.handle(decisionMessage(spacecraftId, currentPlan.planId, attempt.attemptId, PlanDecision.REJECTED))

        assertEquals(PlannedPlanStatus.EXPIRED, currentPlan.status)
        assertEquals(PlanIssueAttemptStatus.REJECTED, attempt.status)
        assertEquals(null, state.activeAttemptId)
        assertEquals(true, nextPlan.issueNotBefore != null)
        verify(recalculationJobService, never()).enqueue(spacecraftId, "PLAN_REJECTED_AFTER_CUTOFF")
    }

    @Test
    fun `stale decision records inactive attempt but does not change plan lifecycle or active state`() {
        val spacecraftId = "25544"
        val plan = plannedPlan(spacecraftId, time(12), time(13), PlannedPlanStatus.WAITING_DECISION)
        val attempt = attempt(plan.planId, status = PlanIssueAttemptStatus.STARTED)
        val activeAttemptId = UUID.randomUUID()
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, activePlanId = plan.planId, activeAttemptId = activeAttemptId)
        val recalculationJobService = mock(PlanRecalculationJobService::class.java)
        val slotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java)
        val useCase = decisionUseCase(
            plan = plan,
            attempt = attempt,
            state = state,
            recalculationJobService = recalculationJobService,
            slotsUpdateJobService = slotsUpdateJobService
        )

        useCase.handle(decisionMessage(spacecraftId, plan.planId, attempt.attemptId, PlanDecision.ACCEPTED))

        assertEquals(PlanIssueAttemptStatus.ACCEPTED, attempt.status)
        assertEquals(PlannedPlanStatus.WAITING_DECISION, plan.status)
        assertEquals(plan.planId, state.activePlanId)
        assertEquals(activeAttemptId, state.activeAttemptId)
        assertEquals(null, state.currentBasePlanId)
        verify(recalculationJobService, never()).enqueue(spacecraftId, "PLAN_ACCEPTED")
        verify(slotsUpdateJobService, never()).enqueue(plan.planId)
    }

    @Test
    fun `terminal attempt is not reprocessed by new decision event`() {
        val spacecraftId = "25544"
        val plan = plannedPlan(spacecraftId, time(12), time(13), PlannedPlanStatus.ACCEPTED)
        val oldEventId = UUID.randomUUID()
        val attempt = attempt(plan.planId, status = PlanIssueAttemptStatus.ACCEPTED)
        attempt.decisionEventId = oldEventId
        val state = SpacecraftPlanningStateEntity(
            spacecraftId = spacecraftId,
            currentBasePlanId = plan.planId,
            activePlanId = null,
            activeAttemptId = null
        )
        val recalculationJobService = mock(PlanRecalculationJobService::class.java)
        val slotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java)
        val useCase = decisionUseCase(
            plan = plan,
            attempt = attempt,
            state = state,
            recalculationJobService = recalculationJobService,
            slotsUpdateJobService = slotsUpdateJobService
        )

        useCase.handle(decisionMessage(spacecraftId, plan.planId, attempt.attemptId, PlanDecision.REJECTED))

        assertEquals(PlanIssueAttemptStatus.ACCEPTED, attempt.status)
        assertEquals(oldEventId, attempt.decisionEventId)
        assertEquals(PlannedPlanStatus.ACCEPTED, plan.status)
        assertEquals(plan.planId, state.currentBasePlanId)
        verify(recalculationJobService, never()).enqueue(spacecraftId, "PLAN_REJECTED_AFTER_CUTOFF")
        verify(slotsUpdateJobService, never()).enqueue(plan.planId)
    }

    private fun decisionUseCase(
        plan: PlannedPlanEntity,
        attempt: PlanIssueAttemptEntity,
        state: SpacecraftPlanningStateEntity,
        processedRepository: ProcessedPlanDecisionEventRepository = processedRepository(),
        recalculationJobService: PlanRecalculationJobService = mock(PlanRecalculationJobService::class.java),
        slotsUpdateJobService: AcceptedPlanSlotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java),
        plannedRepository: PlannedPlanRepository = plannedRepository(linkedMapOf(plan.planId to plan))
    ): HandlePlanDecisionUseCase =
        HandlePlanDecisionUseCase(
            processedPlanDecisionEventRepository = processedRepository,
            planIssueAttemptRepository = attemptRepository(linkedMapOf(attempt.attemptId to attempt)),
            plannedPlanRepository = plannedRepository,
            spacecraftPlanningStateJpaRepository = stateRepository(state),
            planRecalculationJobService = recalculationJobService,
            acceptedPlanSlotsUpdateJobService = slotsUpdateJobService,
            planningProperties = planningProperties()
        )

    private fun issueWorker(
        planStore: MutableMap<UUID, PlannedPlanEntity>,
        attemptStore: MutableMap<UUID, PlanIssueAttemptEntity>,
        beforeStartReturns: (ProcessStartResponse) -> Unit
    ): IssueDuePlanWorker {
        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)
                .also(beforeStartReturns)
        }
        return IssueDuePlanWorker(
            planningProperties = planningProperties(),
            plannedPlanRepository = plannedRepository(planStore),
            planIssueAttemptRepository = attemptRepository(attemptStore),
            spacecraftPlanningStateJpaRepository = stateRepository(SpacecraftPlanningStateEntity(spacecraftId = planStore.values.first().spacecraftId)),
            bpmnPlanIssueProcessStarter = starter,
            transactionTemplate = transactionTemplate()
        )
    }

    private fun processedRepository(): ProcessedPlanDecisionEventRepository {
        val repository = mock(ProcessedPlanDecisionEventRepository::class.java)
        `when`(repository.insertIfAbsent(anyNonNull(), anyNonNull())).thenReturn(1)
        return repository
    }

    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.lockByPlanId(anyNonNull())).thenAnswer { invocation -> store[invocation.arguments[0]] }
        `when`(repository.save(anyNonNull())).thenAnswer { invocation ->
            val plan = invocation.arguments[0] as PlannedPlanEntity
            store[plan.planId] = plan
            plan
        }
        `when`(repository.findDueForUpdate(anyNonNull(), anyNonNull(), org.mockito.ArgumentMatchers.anyInt())).thenAnswer { invocation ->
            val now = invocation.arguments[0] as LocalDateTime
            val issueBefore = invocation.arguments[1] as LocalDateTime
            store.values.filter { plan ->
                plan.status == PlannedPlanStatus.PLANNED &&
                    (plan.issueNotBefore == null || !plan.issueNotBefore!!.isAfter(now)) &&
                    !plan.startTime.isAfter(issueBefore) &&
                    plan.startTime.isAfter(now)
            }.sortedBy { plan -> plan.startTime }
        }
        `when`(repository.findTopBySpacecraftIdAndStatusAndStartTimeAfterOrderByStartTime(anyNonNull(), anyNonNull(), anyNonNull()))
            .thenAnswer { invocation ->
                store.values
                    .filter { plan ->
                        plan.spacecraftId == invocation.arguments[0] &&
                            plan.status == invocation.arguments[1] &&
                            plan.startTime > invocation.arguments[2] as LocalDateTime
                    }
                    .minByOrNull { plan -> plan.startTime }
            }
        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.lockByAttemptId(anyNonNull())).thenAnswer { invocation -> 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 plannedPlan(
        spacecraftId: String,
        start: LocalDateTime,
        end: LocalDateTime,
        status: PlannedPlanStatus = PlannedPlanStatus.PLANNED
    ): PlannedPlanEntity =
        PlannedPlanEntity(
            planId = UUID.randomUUID(),
            spacecraftId = spacecraftId,
            startTime = start,
            endTime = end,
            kppId = "KPP-1",
            status = status,
            issueNotBefore = LocalDateTime.now().minusMinutes(1)
        )

    private fun attempt(planId: UUID, status: PlanIssueAttemptStatus): PlanIssueAttemptEntity =
        PlanIssueAttemptEntity(
            attemptId = UUID.randomUUID(),
            planId = planId,
            attemptNo = 1,
            status = status
        )

    private fun decisionMessage(
        spacecraftId: String,
        planId: UUID,
        attemptId: UUID,
        decision: PlanDecision
    ): PlanDecisionMessage =
        PlanDecisionMessage(
            eventId = UUID.randomUUID(),
            spacecraftId = spacecraftId,
            planId = planId,
            attemptId = attemptId,
            decision = decision,
            decisionTime = LocalDateTime.now(),
            reason = null
        )

    private fun time(hour: Int): LocalDateTime = LocalDateTime.now().plusHours(hour.toLong())

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

    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
    }
}
