package space.nstart.pcp_tgu_service.service

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.mockito.ArgumentMatchers
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.springframework.http.HttpStatus
import org.springframework.web.server.ResponseStatusException
import space.nstart.pcp_tgu_service.domain.CalculatedPlan
import space.nstart.pcp_tgu_service.dto.CreateChainPlanRequest
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.integration.api.MissionBlankRequest
import space.nstart.pcp_tgu_service.integration.api.MissionCloneRequest
import space.nstart.pcp_tgu_service.integration.api.MissionCloneResponse
import space.nstart.pcp_tgu_service.integration.api.MissionPlaningClient
import space.nstart.pcp_tgu_service.repository.PlannedPlanRepository
import space.nstart.pcp_tgu_service.repository.SpacecraftPlanningStateJpaRepository
import java.time.LocalDateTime
import java.util.Optional
import java.util.UUID

class CreateChainPlanUseCaseTest {

    private val spacecraftId = "25544"
    private val kppId = "KPP-1"

    @Test
    fun `with base clones mission into the window and creates a DRAFT with basePlanId and chain preview`() {
        val base = plan(time(1), time(2), PlannedPlanStatus.LAID_IN)
        val newMissionId = UUID.randomUUID()
        val windowStart = time(5)
        val windowStop = time(6)
        val chainTail = listOf(CalculatedPlan(spacecraftId, time(7), time(8), kppId))
        val fixture = fixture(base = base, cloneResult = newMissionId, projectedTail = chainTail, window = windowStart to windowStop)

        val response = fixture.useCase.createChainPlan(
            CreateChainPlanRequest(spacecraftId, base.planId, windowStart, windowStop, kppId)
        )

        assertEquals(newMissionId, response.draftPlanId)
        // Клон по окну (fork-forward-стиль), пустая миссия не создаётся.
        verify(fixture.client).cloneMission(anyUuid(), anyNonNull<MissionCloneRequest>())
        verify(fixture.client, never()).createBlankMission(anyNonNull<MissionBlankRequest>())
        val saved = fixture.lastSaved!!
        assertEquals(newMissionId, saved.planId)
        assertEquals(PlannedPlanStatus.DRAFT, saved.status)
        assertEquals(base.planId, saved.basePlanId)
        assertEquals(windowStart, saved.startTime)
        assertEquals(windowStop, saved.endTime)
        assertEquals(kppId, saved.kppId)
        // Прогноз цепочки от окна вернулся.
        assertEquals(1, response.projectedChain.size)
        assertEquals(PlannedPlanStatus.PLANNED, response.projectedChain.first().status)
    }

    @Test
    fun `without base creates a blank mission and a DRAFT with null basePlanId`() {
        val newMissionId = UUID.randomUUID()
        val fixture = fixture(base = null, blankResult = newMissionId)

        val response = fixture.useCase.createChainPlan(
            CreateChainPlanRequest(spacecraftId, basePlanId = null, time(5), time(6), kppId)
        )

        assertEquals(newMissionId, response.draftPlanId)
        verify(fixture.client).createBlankMission(anyNonNull<MissionBlankRequest>())
        verify(fixture.client, never()).cloneMission(anyUuid(), anyNonNull<MissionCloneRequest>())
        val saved = fixture.lastSaved!!
        assertEquals(newMissionId, saved.planId)
        assertEquals(PlannedPlanStatus.DRAFT, saved.status)
        assertNull(saved.basePlanId)
    }

    @Test
    fun `window not matching any raw ZRV yields 422 and no mission is created`() {
        val fixture = fixture(base = null, rawWindows = listOf(CalculatedPlan(spacecraftId, time(9), time(10), kppId)))
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createChainPlan(CreateChainPlanRequest(spacecraftId, null, time(5), time(6), kppId))
        }
        assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, ex.statusCode)
        verify(fixture.client, never()).createBlankMission(anyNonNull<MissionBlankRequest>())
        verify(fixture.client, never()).cloneMission(anyUuid(), anyNonNull<MissionCloneRequest>())
    }

    @Test
    fun `base belonging to another spacecraft yields 422`() {
        val base = plan(time(1), time(2), PlannedPlanStatus.LAID_IN).apply { spacecraftId = "99999" }
        val fixture = fixture(base = base)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createChainPlan(CreateChainPlanRequest(spacecraftId, base.planId, time(5), time(6), kppId))
        }
        assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, ex.statusCode)
    }

    @Test
    fun `windowStop not after windowStart yields 422`() {
        val fixture = fixture(base = null)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createChainPlan(CreateChainPlanRequest(spacecraftId, null, time(6), time(6), kppId))
        }
        assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, ex.statusCode)
    }

    @Test
    fun `windowStart in the past yields 422`() {
        val fixture = fixture(base = null)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createChainPlan(CreateChainPlanRequest(spacecraftId, null, time(-1), time(1), kppId))
        }
        assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, ex.statusCode)
    }

    @Test
    fun `unknown base plan yields 404`() {
        val fixture = fixture(base = null)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createChainPlan(CreateChainPlanRequest(spacecraftId, UUID.randomUUID(), time(5), time(6), kppId))
        }
        assertEquals(HttpStatus.NOT_FOUND, ex.statusCode)
    }

    @Test
    fun `non-numeric spacecraftId without base yields 422`() {
        val fixture = fixture(base = null, spacecraftIdOverride = "SAT-A")
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createChainPlan(CreateChainPlanRequest("SAT-A", null, time(5), time(6), kppId))
        }
        assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, ex.statusCode)
    }

    @Test
    fun `duplicate window for the same spacecraft yields 409 and no mission is created`() {
        val existing = plan(time(5), time(6), PlannedPlanStatus.DRAFT)
        val fixture = fixture(base = null, existingDuplicate = existing)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createChainPlan(CreateChainPlanRequest(spacecraftId, null, time(5), time(6), kppId))
        }
        assertEquals(HttpStatus.CONFLICT, ex.statusCode)
        verify(fixture.client, never()).createBlankMission(anyNonNull<MissionBlankRequest>())
        verify(fixture.client, never()).cloneMission(anyUuid(), anyNonNull<MissionCloneRequest>())
    }

    @Test
    fun `a failed DRAFT save compensates by deleting the new mission and rethrows`() {
        val newMissionId = UUID.randomUUID()
        val fixture = fixture(base = null, blankResult = newMissionId, failSave = true)
        assertThrows<RuntimeException> {
            fixture.useCase.createChainPlan(CreateChainPlanRequest(spacecraftId, null, time(5), time(6), kppId))
        }
        verify(fixture.client, times(1)).deleteMission(newMissionId)
    }

    // --- helpers ---

    private class Fixture(
        val useCase: CreateChainPlanUseCase,
        val client: MissionPlaningClient,
        private val savedHolder: Array<PlannedPlanEntity?>
    ) {
        val lastSaved: PlannedPlanEntity? get() = savedHolder[0]
    }

    private fun fixture(
        base: PlannedPlanEntity?,
        cloneResult: UUID = UUID.randomUUID(),
        blankResult: UUID = UUID.randomUUID(),
        rawWindows: List<CalculatedPlan>? = null,
        projectedTail: List<CalculatedPlan> = emptyList(),
        failSave: Boolean = false,
        spacecraftIdOverride: String = spacecraftId,
        existingDuplicate: PlannedPlanEntity? = null,
        window: Pair<LocalDateTime, LocalDateTime> = time(5) to time(6)
    ): Fixture {
        val repo = mock(PlannedPlanRepository::class.java)
        `when`(repo.findById(anyUuid())).thenReturn(Optional.ofNullable(base))
        `when`(
            repo.findAllBySpacecraftIdAndStartTimeAndEndTimeAndStatus(
                anyNonNull(), anyNonNull(), anyNonNull(), anyNonNull()
            )
        ).thenReturn(listOfNotNull(existingDuplicate))
        val saved = arrayOfNulls<PlannedPlanEntity>(1)
        `when`(repo.save(anyNonNull<PlannedPlanEntity>())).thenAnswer { inv ->
            if (failSave) throw RuntimeException("DB down")
            val e = inv.arguments[0] as PlannedPlanEntity
            saved[0] = e
            e
        }

        val projectionService = mock(PlanProjectionService::class.java)
        // По умолчанию сырые ЗРВ совпадают с запрашиваемым окном → валидно.
        val defaultRaw = listOf(CalculatedPlan(spacecraftIdOverride, window.first, window.second, kppId))
        `when`(projectionService.rawZrvWindows(spacecraftIdOverride)).thenReturn(rawWindows ?: defaultRaw)
        `when`(projectionService.projectAfterWindow(anyNonNull(), anyNonNull())).thenReturn(projectedTail)

        val client = mock(MissionPlaningClient::class.java)
        `when`(client.cloneMission(anyUuid(), anyNonNull<MissionCloneRequest>()))
            .thenReturn(MissionCloneResponse(newMissionId = cloneResult))
        `when`(client.createBlankMission(anyNonNull<MissionBlankRequest>()))
            .thenReturn(MissionCloneResponse(newMissionId = blankResult))

        val stateRepo = mock(SpacecraftPlanningStateJpaRepository::class.java)
        `when`(stateRepo.findById(spacecraftIdOverride))
            .thenReturn(Optional.of(SpacecraftPlanningStateEntity(spacecraftId = spacecraftIdOverride, chainVersion = 0)))

        val useCase = CreateChainPlanUseCase(repo, projectionService, client, stateRepo)
        return Fixture(useCase, client, saved)
    }

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

    // Фиксированная база времени на инстанс теста: time() детерминирован, окно в запросе и в
    // моке сырых ЗРВ совпадает байт-в-байт (без дрейфа now() между вызовами).
    private val t0: LocalDateTime = LocalDateTime.now()
    private fun time(hour: Int): LocalDateTime = t0.plusHours(hour.toLong())

    @Suppress("UNCHECKED_CAST")
    private fun <T> anyNonNull(): T { ArgumentMatchers.any<T>(); return null as T }
    private fun anyUuid(): UUID { ArgumentMatchers.any<UUID>(); return UUID.randomUUID() }
}
