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.Test
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
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 PlanProjectionServiceTest {

    private val spacecraftId = "25544"

    @Test
    fun `returns empty projection when there is no snapshot`() {
        val calculation = mock(PlanCalculationService::class.java)
        val service = service(snapshot = null, calculation = calculation)

        val result = service.project(spacecraftId)

        assertTrue(result.isEmpty())
        verify(calculation, never()).calculatePlans(anyNonNull())
    }

    @Test
    fun `builds a fresh chain when there is no base plan`() {
        val calculation = mock(PlanCalculationService::class.java)
        val expected = listOf(plan(time(1), time(4)))
        `when`(calculation.calculatePlans(anyNonNull())).thenReturn(expected)
        val service = service(state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId), calculation = calculation)

        val result = service.project(spacecraftId)

        assertEquals(expected, result)
        verify(calculation).calculatePlans(anyNonNull())
        verify(calculation, never()).calculatePlansAfter(anyNonNull(), anyNonNull())
    }

    @Test
    fun `builds the tail after the current base plan`() {
        val base = PlannedPlanEntity(
            planId = UUID.randomUUID(),
            spacecraftId = spacecraftId,
            startTime = time(0),
            endTime = time(3),
            kppId = "KPP-1",
            status = PlannedPlanStatus.AWAITING_LAYIN
        )
        val calculation = mock(PlanCalculationService::class.java)
        val expected = listOf(plan(time(2), time(5)))
        `when`(calculation.calculatePlansAfter(anyNonNull(), anyNonNull())).thenReturn(expected)
        val service = service(
            state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, currentBasePlanId = base.planId),
            basePlan = base,
            calculation = calculation
        )

        val result = service.project(spacecraftId)

        assertEquals(expected, result)
        verify(calculation).calculatePlansAfter(anyNonNull(), anyNonNull())
        verify(calculation, never()).calculatePlans(anyNonNull())
    }

    @Test
    fun `anchors the tail after the in-flight issued plan, not the accepted base`() {
        val base = persistedPlan(time(0), time(3), "KPP-base", PlannedPlanStatus.AWAITING_LAYIN)
        val inFlight = persistedPlan(time(2), time(5), "KPP-inflight", PlannedPlanStatus.WAITING_DECISION)
        val calculation = mock(PlanCalculationService::class.java)
        val expected = listOf(plan(time(4), time(7)))
        var anchor: CalculatedPlan? = null
        `when`(calculation.calculatePlansAfter(anyNonNull(), anyNonNull())).thenAnswer { invocation ->
            anchor = invocation.getArgument(1)
            expected
        }
        val service = service(
            state = SpacecraftPlanningStateEntity(
                spacecraftId = spacecraftId,
                currentBasePlanId = base.planId,
                activePlanId = inFlight.planId
            ),
            plansById = mapOf(base.planId to base, inFlight.planId to inFlight),
            calculation = calculation
        )

        val result = service.project(spacecraftId)

        assertEquals(expected, result)
        assertEquals(inFlight.startTime, anchor?.startTime)
        assertEquals(inFlight.endTime, anchor?.endTime)
        assertEquals(inFlight.kppId, anchor?.kppId)
    }

    @Test
    fun `anchors on the furthest accepted window among several not-yet-laid-in plans`() {
        // Принят и базовый, и более поздняя коррекция (оба AWAITING_LAYIN) — якорь берём по крайнему
        // окну (endTime), чтобы форвард-цепочка строилась после самого дальнего зафиксированного слота.
        val base = persistedPlan(time(0), time(3), "KPP-base", PlannedPlanStatus.AWAITING_LAYIN)
        val accepted = persistedPlan(time(2), time(5), "KPP-accepted", PlannedPlanStatus.AWAITING_LAYIN)
        val calculation = mock(PlanCalculationService::class.java)
        var anchor: CalculatedPlan? = null
        `when`(calculation.calculatePlansAfter(anyNonNull(), anyNonNull())).thenAnswer { invocation ->
            anchor = invocation.getArgument(1)
            emptyList<CalculatedPlan>()
        }
        val service = service(
            state = SpacecraftPlanningStateEntity(
                spacecraftId = spacecraftId,
                currentBasePlanId = base.planId
            ),
            plansById = mapOf(base.planId to base, accepted.planId to accepted),
            calculation = calculation
        )

        service.project(spacecraftId)

        assertEquals(accepted.startTime, anchor?.startTime)
        assertEquals(accepted.kppId, anchor?.kppId)
    }

    @Test
    fun `returns empty projection when the chain is paused after reject`() {
        val calculation = mock(PlanCalculationService::class.java)
        val service = service(
            state = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, pausedAfterReject = true),
            calculation = calculation
        )

        val result = service.project(spacecraftId)

        assertTrue(result.isEmpty())
        verify(calculation, never()).calculatePlans(anyNonNull())
        verify(calculation, never()).calculatePlansAfter(anyNonNull(), anyNonNull())
    }

    private fun service(
        snapshot: SpacecraftVisibilitySnapshotEntity? = snapshot(),
        state: SpacecraftPlanningStateEntity? = null,
        basePlan: PlannedPlanEntity? = null,
        plansById: Map<UUID, PlannedPlanEntity>? = null,
        calculation: PlanCalculationService
    ): PlanProjectionService {
        val snapshotRepository = mock(SpacecraftVisibilitySnapshotRepository::class.java)
        `when`(snapshotRepository.findById(spacecraftId)).thenReturn(Optional.ofNullable(snapshot))
        val stateRepository = mock(SpacecraftPlanningStateJpaRepository::class.java)
        `when`(stateRepository.findById(spacecraftId)).thenReturn(Optional.ofNullable(state))
        val plannedRepository = mock(PlannedPlanRepository::class.java)
        if (plansById != null) {
            `when`(plannedRepository.findById(anyNonNull())).thenAnswer { invocation ->
                Optional.ofNullable(plansById[invocation.getArgument(0)])
            }
            `when`(plannedRepository.findAllBySpacecraftIdAndStatusIn(anyNonNull(), anyNonNull())).thenAnswer { invocation ->
                @Suppress("UNCHECKED_CAST")
                val statuses = invocation.getArgument<Collection<PlannedPlanStatus>>(1)
                plansById.values.filter { plan -> plan.status in statuses }
            }
        } else {
            `when`(plannedRepository.findById(anyNonNull())).thenReturn(Optional.ofNullable(basePlan))
        }
        val parser = mock(VisibilityPayloadParser::class.java)
        `when`(parser.parse(anyNonNull(), anyNonNull())).thenReturn(SpacecraftPoints(spacecraftId, emptyList()))
        return PlanProjectionService(snapshotRepository, stateRepository, plannedRepository, parser, calculation)
    }

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

    private fun snapshot(): SpacecraftVisibilitySnapshotEntity =
        SpacecraftVisibilitySnapshotEntity(
            spacecraftId = spacecraftId,
            noradId = 25544L,
            payloadJson = "[]",
            payloadHash = "hash",
            fetchedAt = LocalDateTime.now()
        )

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

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

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