package space.nstart.pcp_tgu_service.service

import org.junit.jupiter.api.Assertions.assertEquals
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.config.PlanningProperties
import space.nstart.pcp_tgu_service.domain.CalculatedPlan
import space.nstart.pcp_tgu_service.dto.CreateDraftRequest
import space.nstart.pcp_tgu_service.dto.DraftMode
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.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 CreateDraftUseCaseTest {

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

    // --- replace ---

    @Test
    fun `replace creates DRAFT inheriting the source's base (predecessor), not the source itself`() {
        // База исходного плана — это его предшественник в цепочке. Replace-черновик — коррекция,
        // занимающая тот же слот, поэтому он наследует ИМЕННО предшественника, а не сам исходный план.
        val predecessor = UUID.randomUUID()
        val base = plan(time(3), time(6), PlannedPlanStatus.AWAITING_LAYIN, basePlanId = predecessor)
        val newMissionId = UUID.randomUUID()
        val fixture = fixture(base, cloneResult = newMissionId)

        val response = fixture.useCase.createDraft(base.planId, CreateDraftRequest(mode = DraftMode.REPLACE))

        assertEquals(newMissionId, response.draftPlanId)
        val saved = fixture.lastSaved!!
        assertEquals(newMissionId, saved.planId)
        assertEquals(PlannedPlanStatus.DRAFT, saved.status)
        assertEquals(predecessor, saved.basePlanId)
        assertEquals(base.startTime, saved.startTime)
        assertEquals(base.endTime, saved.endTime)
        assertEquals(base.kppId, saved.kppId)
    }

    @Test
    fun `replace of a chain-start plan inherits a null base`() {
        val base = plan(time(3), time(6), PlannedPlanStatus.AWAITING_LAYIN, basePlanId = null)
        val fixture = fixture(base)

        fixture.useCase.createDraft(base.planId, CreateDraftRequest(mode = DraftMode.REPLACE))

        assertEquals(null, fixture.lastSaved!!.basePlanId)
    }

    @Test
    fun `draft snapshots the current chain version as its optimistic token`() {
        val base = plan(time(3), time(6), PlannedPlanStatus.AWAITING_LAYIN)
        val fixture = fixture(
            base,
            planningState = SpacecraftPlanningStateEntity(spacecraftId = spacecraftId, chainVersion = 7)
        )

        fixture.useCase.createDraft(base.planId, CreateDraftRequest(mode = DraftMode.REPLACE))

        assertEquals(7L, fixture.lastSaved!!.chainVersion)
    }

    @Test
    fun `replace rejects a DRAFT base`() {
        val base = plan(time(3), time(6), PlannedPlanStatus.DRAFT)
        val fixture = fixture(base)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createDraft(base.planId, CreateDraftRequest(mode = DraftMode.REPLACE))
        }
        assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, ex.statusCode)
    }

    @Test
    fun `replace rejects a LAID_IN base`() {
        val base = plan(time(3), time(6), PlannedPlanStatus.LAID_IN)
        val fixture = fixture(base)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createDraft(base.planId, CreateDraftRequest(mode = DraftMode.REPLACE))
        }
        assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, ex.statusCode)
    }

    @Test
    fun `replace rejects a base inside the freeze window`() {
        // startTime через 5 минут, заморозка 10 минут → точка невозврата пройдена.
        val base = plan(time(0).plusMinutes(5), time(3), PlannedPlanStatus.ACCEPTED)
        val fixture = fixture(base)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createDraft(base.planId, CreateDraftRequest(mode = DraftMode.REPLACE))
        }
        assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, ex.statusCode)
        verify(fixture.client, never()).cloneMission(anyUuid(), anyNonNull())
    }

    // --- fork-forward ---

    @Test
    fun `fork-forward creates DRAFT on a window matching the projection`() {
        val base = plan(time(0).plusMinutes(5), time(1), PlannedPlanStatus.LAID_IN)
        val windowStart = time(5)
        val windowStop = time(6)
        val newMissionId = UUID.randomUUID()
        val fixture = fixture(
            base,
            cloneResult = newMissionId,
            projection = listOf(CalculatedPlan(spacecraftId, windowStart, windowStop, kppId))
        )

        val response = fixture.useCase.createDraft(
            base.planId,
            CreateDraftRequest(mode = DraftMode.FORK_FORWARD, windowStart = windowStart, windowStop = windowStop, windowKppId = kppId)
        )

        assertEquals(newMissionId, response.draftPlanId)
        val saved = fixture.lastSaved!!
        assertEquals(windowStart, saved.startTime)
        assertEquals(windowStop, saved.endTime)
        assertEquals(kppId, saved.kppId)
        assertEquals(base.planId, saved.basePlanId)
    }

    @Test
    fun `fork-forward requires windowStart, windowStop and windowKppId`() {
        val base = plan(time(3), time(6), PlannedPlanStatus.LAID_IN)
        val fixture = fixture(base)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createDraft(base.planId, CreateDraftRequest(mode = DraftMode.FORK_FORWARD))
        }
        assertEquals(HttpStatus.BAD_REQUEST, ex.statusCode)
    }

    @Test
    fun `fork-forward rejects a windowStart not strictly in the future`() {
        val base = plan(time(3), time(6), PlannedPlanStatus.LAID_IN)
        val fixture = fixture(base)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createDraft(
                base.planId,
                CreateDraftRequest(mode = DraftMode.FORK_FORWARD, windowStart = time(-1), windowStop = time(1), windowKppId = kppId)
            )
        }
        assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, ex.statusCode)
    }

    @Test
    fun `fork-forward rejects a window that matches no future ZRV in the projection`() {
        val base = plan(time(0).plusMinutes(5), time(1), PlannedPlanStatus.LAID_IN)
        val fixture = fixture(
            base,
            projection = listOf(CalculatedPlan(spacecraftId, time(9), time(10), kppId))
        )
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createDraft(
                base.planId,
                CreateDraftRequest(mode = DraftMode.FORK_FORWARD, windowStart = time(5), windowStop = time(6), windowKppId = kppId)
            )
        }
        assertEquals(HttpStatus.UNPROCESSABLE_ENTITY, ex.statusCode)
        verify(fixture.client, never()).cloneMission(anyUuid(), anyNonNull())
    }

    // --- общие ---

    @Test
    fun `unknown mode is rejected`() {
        val base = plan(time(3), time(6), PlannedPlanStatus.AWAITING_LAYIN)
        val fixture = fixture(base)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createDraft(base.planId, CreateDraftRequest(mode = "weird"))
        }
        assertEquals(HttpStatus.BAD_REQUEST, ex.statusCode)
    }

    @Test
    fun `unknown base plan yields 404`() {
        val fixture = fixture(base = null)
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createDraft(UUID.randomUUID(), CreateDraftRequest(mode = DraftMode.REPLACE))
        }
        assertEquals(HttpStatus.NOT_FOUND, ex.statusCode)
    }

    @Test
    fun `an existing open DRAFT on the same slot (spacecraft + window) yields 409`() {
        val base = plan(time(3), time(6), PlannedPlanStatus.AWAITING_LAYIN)
        val existing = plan(time(3), time(6), PlannedPlanStatus.DRAFT)
        val fixture = fixture(base, existingDrafts = listOf(existing))
        val ex = assertThrows<ResponseStatusException> {
            fixture.useCase.createDraft(base.planId, CreateDraftRequest(mode = DraftMode.REPLACE))
        }
        assertEquals(HttpStatus.CONFLICT, ex.statusCode)
        verify(fixture.client, never()).cloneMission(anyUuid(), anyNonNull())
    }

    @Test
    fun `a failed DRAFT save compensates by deleting the cloned mission and rethrows`() {
        val base = plan(time(3), time(6), PlannedPlanStatus.AWAITING_LAYIN)
        val newMissionId = UUID.randomUUID()
        val fixture = fixture(base, cloneResult = newMissionId, failSave = true)

        assertThrows<RuntimeException> {
            fixture.useCase.createDraft(base.planId, CreateDraftRequest(mode = DraftMode.REPLACE))
        }
        verify(fixture.client, times(1)).deleteMission(newMissionId)
    }

    // --- helpers ---

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

    private fun fixture(
        base: PlannedPlanEntity?,
        cloneResult: UUID = UUID.randomUUID(),
        projection: List<CalculatedPlan> = emptyList(),
        existingDrafts: List<PlannedPlanEntity> = emptyList(),
        failSave: Boolean = false,
        planningState: SpacecraftPlanningStateEntity? = null
    ): Fixture {
        val repo = mock(PlannedPlanRepository::class.java)
        `when`(repo.findById(anyUuid())).thenReturn(Optional.ofNullable(base))
        `when`(
            repo.findAllBySpacecraftIdAndStartTimeAndEndTimeAndStatus(
                anyNonNull(), anyNonNull(), anyNonNull(), anyNonNull()
            )
        ).thenReturn(existingDrafts)

        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)
        `when`(projectionService.project(spacecraftId)).thenReturn(projection)

        val client = mock(MissionPlaningClient::class.java)
        `when`(client.cloneMission(anyUuid(), anyNonNull()))
            .thenReturn(MissionCloneResponse(newMissionId = cloneResult))

        val stateRepo = mock(SpacecraftPlanningStateJpaRepository::class.java)
        `when`(stateRepo.findById(spacecraftId)).thenReturn(Optional.ofNullable(planningState))

        val useCase = CreateDraftUseCase(repo, projectionService, client, props(freeze = 10), stateRepo)
        return Fixture(useCase, client, saved)
    }

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

    private fun props(freeze: Long): PlanningProperties =
        PlanningProperties(30, 180, 60, false, 10, 14, 0, 30_000, 5, 30, 300, 30, freeze)

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

    // Kotlin non-null матчеры: ArgumentMatchers.any() возвращает null и роняет NPE на non-null параметрах.
    @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() }
}
