package space.nstart.pcp_tgu_service.service

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
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.PlanDecision
import space.nstart.pcp_tgu_service.dto.PlanDecisionMessage
import space.nstart.pcp_tgu_service.dto.PlanExecutionFeedbackMessage
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 space.nstart.pcp_tgu_service.repository.SpacecraftSnapshotRepository
import java.time.LocalDateTime
import java.util.Optional
import java.util.UUID

class PlanIssueAndDecisionFlowTest {

    @Test
    fun `issuePlanNow creates one active attempt and repeated manual issue creates a new attempt after state cleared`() {
        val spacecraftId = "25544"
        val plan = plannedPlan(spacecraftId, time(12), time(13), PlannedPlanStatus.PLANNED)
        val planStore = linkedMapOf(plan.planId to plan)
        val attemptStore = linkedMapOf<UUID, PlanIssueAttemptEntity>()
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId)
        val worker = issueWorker(planStore, attemptStore, state)

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

        assertEquals(1, attemptStore.getValue(firstResponse.attemptId).attemptNo)
        assertEquals(2, attemptStore.getValue(secondResponse.attemptId).attemptNo)
        assertNotEquals(firstResponse.attemptId, secondResponse.attemptId)
        assertEquals(PlannedPlanStatus.WAITING_DECISION, plan.status)
        assertTrue(blocked?.message?.startsWith("404 NOT_FOUND") == true)
    }

    @Test
    fun `accepted decision moves plan to AWAITING_LAYIN sets base and enqueues slots update once`() {
        val spacecraftId = "25544"
        val plan = plannedPlan(spacecraftId, time(12), time(13), PlannedPlanStatus.WAITING_DECISION)
        val attempt = attempt(plan.planId, 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 slotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java)
        val useCase = decisionUseCase(plan, attempt, state, processedRepository, slotsUpdateJobService)
        val message = decisionMessage(spacecraftId, plan.planId, attempt.attemptId, PlanDecision.ACCEPTED)

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

        assertEquals(PlanIssueAttemptStatus.ACCEPTED, attempt.status)
        assertEquals(PlannedPlanStatus.AWAITING_LAYIN, plan.status)
        assertEquals(plan.planId, state.currentBasePlanId)
        assertNull(state.activePlanId)
        assertNull(state.activeAttemptId)
        assertNull(state.urgentReissueFrom)
        verify(slotsUpdateJobService).enqueue(plan.planId)
    }

    @Test
    fun `rejected decision marks plan REJECTED and arms urgent reissue`() {
        val spacecraftId = "25544"
        val plan = plannedPlan(spacecraftId, LocalDateTime.now().plusHours(2), LocalDateTime.now().plusHours(3), PlannedPlanStatus.WAITING_DECISION)
        val attempt = attempt(plan.planId, PlanIssueAttemptStatus.STARTED)
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, activePlanId = plan.planId, activeAttemptId = attempt.attemptId)
        val slotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java)
        val useCase = decisionUseCase(plan, attempt, state, slotsUpdateJobService = slotsUpdateJobService)

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

        assertEquals(PlanIssueAttemptStatus.REJECTED, attempt.status)
        assertEquals(PlannedPlanStatus.REJECTED, plan.status)
        assertNull(state.activeAttemptId)
        assertNotNull(state.urgentReissueFrom)
        verify(slotsUpdateJobService, never()).enqueue(plan.planId)
    }

    @Test
    fun `layin failed on current base marks LAYIN_FAILED and arms urgent reissue idempotently`() {
        assertNegativeFeedbackArmsReissue(PlannedPlanStatus.LAYIN_FAILED) { useCase, message ->
            useCase.handleLayinFailed(message)
        }
    }

    @Test
    fun `plan not accepted on current base marks NOT_ACCEPTED and arms urgent reissue idempotently`() {
        assertNegativeFeedbackArmsReissue(PlannedPlanStatus.NOT_ACCEPTED) { useCase, message ->
            useCase.handleNotAccepted(message)
        }
    }

    @Test
    fun `negative feedback for a plan that is not the current base is ignored`() {
        val spacecraftId = "25544"
        val base = plannedPlan(spacecraftId, time(20), time(44), PlannedPlanStatus.AWAITING_LAYIN)
        val stalePlan = plannedPlan(spacecraftId, time(2), time(3), PlannedPlanStatus.REJECTED)
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, currentBasePlanId = base.planId)
        val useCase = HandlePlanDecisionUseCase(
            processedPlanDecisionEventRepository = processedRepository(),
            planIssueAttemptRepository = attemptRepository(linkedMapOf()),
            plannedPlanRepository = plannedRepository(linkedMapOf(base.planId to base, stalePlan.planId to stalePlan)),
            spacecraftPlanningStateJpaRepository = stateRepository(state),
            acceptedPlanSlotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java),
            planningProperties = planningProperties()
        )

        useCase.handleLayinFailed(feedbackMessage(spacecraftId, stalePlan.planId))

        assertEquals(PlannedPlanStatus.REJECTED, stalePlan.status)
        assertNull(state.urgentReissueFrom)
    }

    @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, PlanIssueAttemptStatus.STARTED)
        val activeAttemptId = UUID.randomUUID()
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, activePlanId = plan.planId, activeAttemptId = activeAttemptId)
        val slotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java)
        val useCase = decisionUseCase(plan, attempt, state, 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)
        assertNull(state.currentBasePlanId)
        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.AWAITING_LAYIN)
        val attempt = attempt(plan.planId, PlanIssueAttemptStatus.ACCEPTED)
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, currentBasePlanId = plan.planId)
        val slotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java)
        val useCase = decisionUseCase(plan, attempt, state, slotsUpdateJobService = slotsUpdateJobService)

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

        assertEquals(PlanIssueAttemptStatus.ACCEPTED, attempt.status)
        assertEquals(PlannedPlanStatus.AWAITING_LAYIN, plan.status)
        assertEquals(plan.planId, state.currentBasePlanId)
        assertNull(state.urgentReissueFrom)
        verify(slotsUpdateJobService, never()).enqueue(plan.planId)
    }

    private fun assertNegativeFeedbackArmsReissue(
        expectedStatus: PlannedPlanStatus,
        invoke: (HandlePlanDecisionUseCase, PlanExecutionFeedbackMessage) -> Unit
    ) {
        val spacecraftId = "25544"
        val base = plannedPlan(spacecraftId, time(20), time(44), PlannedPlanStatus.AWAITING_LAYIN)
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, currentBasePlanId = base.planId)
        val processedRepository = mock(ProcessedPlanDecisionEventRepository::class.java)
        `when`(processedRepository.insertIfAbsent(anyNonNull(), anyNonNull())).thenReturn(1).thenReturn(0)
        val useCase = HandlePlanDecisionUseCase(
            processedPlanDecisionEventRepository = processedRepository,
            planIssueAttemptRepository = attemptRepository(linkedMapOf()),
            plannedPlanRepository = plannedRepository(linkedMapOf(base.planId to base)),
            spacecraftPlanningStateJpaRepository = stateRepository(state),
            acceptedPlanSlotsUpdateJobService = mock(AcceptedPlanSlotsUpdateJobService::class.java),
            planningProperties = planningProperties()
        )
        val message = feedbackMessage(spacecraftId, base.planId)

        invoke(useCase, message)
        invoke(useCase, message)

        assertEquals(expectedStatus, base.status)
        assertNotNull(state.urgentReissueFrom)
    }

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

    private fun issueWorker(
        planStore: MutableMap<UUID, PlannedPlanEntity>,
        attemptStore: MutableMap<UUID, PlanIssueAttemptEntity>,
        state: SpacecraftPlanningStateEntity
    ): 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)
        }
        return IssueDuePlanWorker(
            planningProperties = planningProperties(),
            spacecraftSnapshotRepository = mock(SpacecraftSnapshotRepository::class.java),
            plannedPlanRepository = plannedRepository(planStore),
            planIssueAttemptRepository = attemptRepository(attemptStore),
            spacecraftPlanningStateJpaRepository = stateRepository(state),
            planProjectionService = mock(PlanProjectionService::class.java),
            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
        }
        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
    ): 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 feedbackMessage(spacecraftId: String, planId: UUID): PlanExecutionFeedbackMessage =
        PlanExecutionFeedbackMessage(
            eventId = UUID.randomUUID(),
            spacecraftId = spacecraftId,
            planId = planId,
            occurredAt = 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, 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
    }
}
