package space.nstart.pcp_tgu_service.service

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.`when`
import space.nstart.pcp_tgu_service.config.PlanningProperties
import space.nstart.pcp_tgu_service.domain.CalculatedPlan
import space.nstart.pcp_tgu_service.domain.SpacecraftPoints
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.SpacecraftVisibilitySnapshotEntity
import space.nstart.pcp_tgu_service.integration.api.VisibilityPayloadParser
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftVisibilitySnapshotRepository
import java.time.LocalDateTime
import java.util.Optional
import java.util.UUID

class RebuildPlanScheduleUseCaseTest {

    @Test
    fun `rebuild keeps stable planId by business key and supersedes obsolete planned only`() {
        val spacecraftId = "25544"
        val planStore = linkedMapOf<UUID, PlannedPlanEntity>()
        // Устаревший PLANNED-план, который уже стартовал, но ещё не завершился —
        // должен быть погашен по новому условию end_time > now (а не start_time > now).
        val stalePlanId = UUID.randomUUID()
        planStore[stalePlanId] = PlannedPlanEntity(
            planId = stalePlanId,
            spacecraftId = spacecraftId,
            startTime = LocalDateTime.now().minusHours(1),
            endTime = LocalDateTime.now().plusHours(1),
            kppId = "KPP-STALE",
            status = PlannedPlanStatus.PLANNED
        )
        val plannedRepository = plannedRepository(planStore)
        val state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId)
        val stateRepository = stateRepository(state)
        val snapshotRepository = mock(SpacecraftVisibilitySnapshotRepository::class.java)
        `when`(snapshotRepository.findById(spacecraftId)).thenReturn(
            Optional.of(
                SpacecraftVisibilitySnapshotEntity(
                    spacecraftId = spacecraftId,
                    noradId = 25544L,
                    payloadJson = rawPayload(),
                    payloadHash = "hash",
                    fetchedAt = LocalDateTime.now()
                )
            )
        )
        val calculationService = mock(PlanCalculationService::class.java)
        val baseTime = LocalDateTime.now().plusHours(1)
        val firstPlan = CalculatedPlan(spacecraftId, baseTime, baseTime.plusHours(2), "KPP-1")
        val secondPlan = CalculatedPlan(spacecraftId, baseTime.plusHours(3), baseTime.plusHours(5), "KPP-2")
        `when`(calculationService.calculatePlans(anyNonNull())).thenReturn(listOf(firstPlan, secondPlan))
        val service = RebuildPlanScheduleUseCase(
            spacecraftVisibilitySnapshotRepository = snapshotRepository,
            spacecraftPlanningStateJpaRepository = stateRepository,
            plannedPlanRepository = plannedRepository,
            visibilityPayloadParser = VisibilityPayloadParser(jacksonObjectMapper().findAndRegisterModules()),
            planCalculationService = calculationService,
            planningProperties = planningProperties()
        )

        service.rebuild(spacecraftId)
        val firstPlanId = planStore.values.first { plan -> plan.kppId == "KPP-1" }.planId
        // Устаревший активный план без места в новой цепочке погашен уже на первом rebuild.
        assertEquals(PlannedPlanStatus.SUPERSEDED, planStore.getValue(stalePlanId).status)

        `when`(calculationService.calculatePlans(anyNonNull())).thenReturn(listOf(firstPlan))
        service.rebuild(spacecraftId)

        assertEquals(firstPlanId, planStore.values.first { plan -> plan.kppId == "KPP-1" }.planId)
        assertEquals(PlannedPlanStatus.SUPERSEDED, planStore.values.first { plan -> plan.kppId == "KPP-2" }.status)
        assertNotEquals(planStore.values.first { plan -> plan.kppId == "KPP-1" }.planId, planStore.values.first { plan -> plan.kppId == "KPP-2" }.planId)
        assertEquals(2, state.chainVersion)
    }

    private fun plannedRepository(store: MutableMap<UUID, PlannedPlanEntity>): PlannedPlanRepository {
        val repository = mock(PlannedPlanRepository::class.java)
        `when`(
            repository.upsertPlannedByBusinessKey(
                anyNonNull(),
                anyNonNull(),
                anyNonNull(),
                anyNonNull(),
                anyNonNull(),
                anyNonNull(),
                anyNonNull(),
                org.mockito.ArgumentMatchers.anyLong(),
                anyNonNull()
            )
        ).thenAnswer { invocation ->
            val planId = invocation.arguments[0] as UUID
            val spacecraftId = invocation.arguments[1] as String
            val startTime = invocation.arguments[2] as LocalDateTime
            val endTime = invocation.arguments[3] as LocalDateTime
            val kppId = invocation.arguments[4] as String
            val issueNotBefore = invocation.arguments[5] as LocalDateTime
            val basePlanId = invocation.arguments[6] as UUID?
            val chainVersion = invocation.arguments[7] as Long
            val now = invocation.arguments[8] as LocalDateTime
            val existing = store.values.firstOrNull { plan ->
                plan.spacecraftId == spacecraftId &&
                    plan.startTime == startTime &&
                    plan.endTime == endTime &&
                    plan.kppId == kppId
            }

            if (existing != null) {
                if (existing.status == PlannedPlanStatus.SUPERSEDED || existing.status == PlannedPlanStatus.EXPIRED) {
                    existing.status = PlannedPlanStatus.PLANNED
                }
                if (existing.status == PlannedPlanStatus.PLANNED) {
                    existing.issueNotBefore = issueNotBefore
                    existing.basePlanId = basePlanId
                    existing.chainVersion = chainVersion
                    existing.updatedAt = now
                }
                existing
            } else {
                PlannedPlanEntity(
                    planId = planId,
                    spacecraftId = spacecraftId,
                    startTime = startTime,
                    endTime = endTime,
                    kppId = kppId,
                    status = PlannedPlanStatus.PLANNED,
                    issueNotBefore = issueNotBefore,
                    basePlanId = basePlanId,
                    chainVersion = chainVersion,
                    createdAt = now,
                    updatedAt = now
                ).also { plan -> store[plan.planId] = plan }
            }
        }
        `when`(repository.findBySpacecraftIdAndStartTimeAndEndTimeAndKppId(anyNonNull(), anyNonNull(), anyNonNull(), anyNonNull()))
            .thenAnswer { invocation ->
                store.values.firstOrNull { plan ->
                    plan.spacecraftId == invocation.arguments[0] &&
                        plan.startTime == invocation.arguments[1] &&
                        plan.endTime == invocation.arguments[2] &&
                        plan.kppId == invocation.arguments[3]
                }
            }
        `when`(repository.save(anyNonNull())).thenAnswer { invocation ->
            val plan = invocation.arguments[0] as PlannedPlanEntity
            store[plan.planId] = plan
            plan
        }
        `when`(repository.findAllBySpacecraftIdAndStatusAndEndTimeAfterOrderByStartTime(anyNonNull(), anyNonNull(), anyNonNull()))
            .thenAnswer { invocation ->
                store.values
                    .filter { plan ->
                        plan.spacecraftId == invocation.arguments[0] &&
                            plan.status == invocation.arguments[1] &&
                            plan.endTime > invocation.arguments[2] as LocalDateTime
                    }
                    .sortedBy { plan -> plan.startTime }
            }
        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 rawPayload(): String =
        """[{"noradId":25544,"stationId":7,"onStart":{"time":"2026-05-26T10:00:00Z","elevation":1.0},"onMaximum":{"time":"2026-05-26T10:05:00Z","elevation":45.0},"onStop":{"time":"2026-05-26T10:10:00Z","elevation":1.0}}]"""

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

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