package org.nstart.dep265.requestservice.service

import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.mockingDetails
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions
import org.nstart.dep265.requestservice.config.RequestExpiryProperties
import org.nstart.dep265.requestservice.entity.RequestEntity
import org.nstart.dep265.requestservice.entity.RequestStatus
import org.nstart.dep265.requestservice.entity.RequestSurveyType
import org.nstart.dep265.requestservice.repository.OutboxEventRepository
import org.nstart.dep265.requestservice.repository.RequestRepository
import org.springframework.transaction.TransactionStatus
import org.springframework.transaction.support.SimpleTransactionStatus
import org.springframework.transaction.support.TransactionCallback
import org.springframework.transaction.support.TransactionOperations
import java.time.LocalDateTime
import java.util.UUID

class RequestExpiryServiceTest {

    @Test
    fun `expires overdue accepted and active requests and emits terminal events`() {
        val requestRepository = mock(RequestRepository::class.java)
        val outboxEventRepository = mock(OutboxEventRepository::class.java)
        val service = expiryService(requestRepository, outboxEventRepository, RequestExpiryProperties(batchSize = 200))

        val expiredAccepted = overdueRequest(status = RequestStatus.ACCEPTED)
        val expiredActive = overdueRequest(status = RequestStatus.ACTIVE)
        doReturn(listOf(expiredAccepted, expiredActive))
            .`when`(requestRepository)
            .findExpirableForUpdateSkipLocked(anyLocalDateTime(), anyInt())

        val expiredCount = service.expireOverdue()

        assertEquals(2, expiredCount)
        assertEquals(RequestStatus.EXPIRED, expiredAccepted.status)
        assertEquals(RequestStatus.EXPIRED, expiredActive.status)
        verify(requestRepository).save(expiredAccepted)
        verify(requestRepository).save(expiredActive)

        // По каждой истёкшей заявке — терминальное событие в outbox для geoportal.
        val terminalInserts = mockingDetails(outboxEventRepository).invocations
            .filter { invocation -> invocation.method.name == "insertIfAbsent" }
        assertEquals(2, terminalInserts.size)
        assertTrue(terminalInserts.all { invocation -> invocation.arguments[1] == "REQUEST_TERMINAL_STATUS" })
    }

    @Test
    fun `keeps fetching while a full batch is returned`() {
        val requestRepository = mock(RequestRepository::class.java)
        val service = expiryService(
            requestRepository,
            mock(OutboxEventRepository::class.java),
            RequestExpiryProperties(batchSize = 1),
        )

        val first = overdueRequest(status = RequestStatus.ACTIVE)
        val second = overdueRequest(status = RequestStatus.ACCEPTED)
        doReturn(listOf(first), listOf(second), emptyList<RequestEntity>())
            .`when`(requestRepository)
            .findExpirableForUpdateSkipLocked(anyLocalDateTime(), anyInt())

        val expiredCount = service.expireOverdue()

        assertEquals(2, expiredCount)
        verify(requestRepository, times(3))
            .findExpirableForUpdateSkipLocked(anyLocalDateTime(), anyInt())
    }

    @Test
    fun `empty sweep returns zero and emits nothing`() {
        val requestRepository = mock(RequestRepository::class.java)
        val outboxEventRepository = mock(OutboxEventRepository::class.java)
        val service = expiryService(requestRepository, outboxEventRepository, RequestExpiryProperties(batchSize = 200))

        doReturn(emptyList<RequestEntity>())
            .`when`(requestRepository)
            .findExpirableForUpdateSkipLocked(anyLocalDateTime(), anyInt())

        val expiredCount = service.expireOverdue()

        assertEquals(0, expiredCount)
        verify(requestRepository, never()).save(any(RequestEntity::class.java))
        verifyNoInteractions(outboxEventRepository)
    }

    @Test
    fun `request still inside its window is not expired`() {
        val requestRepository = mock(RequestRepository::class.java)
        val outboxEventRepository = mock(OutboxEventRepository::class.java)
        val service = expiryService(requestRepository, outboxEventRepository, RequestExpiryProperties(batchSize = 200))

        val now = LocalDateTime.now()
        val inWindow = request(
            status = RequestStatus.ACTIVE,
            beginDateTime = now.minusDays(1),
            endDateTime = now.plusDays(1),
        )
        doReturn(listOf(inWindow))
            .`when`(requestRepository)
            .findExpirableForUpdateSkipLocked(anyLocalDateTime(), anyInt())

        val expiredCount = service.expireOverdue()

        assertEquals(0, expiredCount)
        assertEquals(RequestStatus.ACTIVE, inWindow.status)
        verify(requestRepository, never()).save(any(RequestEntity::class.java))
        verifyNoInteractions(outboxEventRepository)
    }

    @Test
    fun `disabled scheduler does not sweep`() {
        val requestRepository = mock(RequestRepository::class.java)
        val service = expiryService(
            requestRepository,
            mock(OutboxEventRepository::class.java),
            RequestExpiryProperties(enabled = false),
        )

        service.expireOverdueScheduledBatch()

        verify(requestRepository, never())
            .findExpirableForUpdateSkipLocked(anyLocalDateTime(), anyInt())
    }

    private fun expiryService(
        requestRepository: RequestRepository,
        outboxEventRepository: OutboxEventRepository,
        properties: RequestExpiryProperties,
    ): RequestExpiryService {
        val geometryService = GeometryService()
        return RequestExpiryService(
            requestRepository = requestRepository,
            requestLifecycleService = RequestLifecycleService(geometryService),
            requestStatusOutboxService = RequestStatusOutboxService(
                outboxEventRepository = outboxEventRepository,
                objectMapper = jacksonObjectMapper()
                    .registerModule(JavaTimeModule())
                    .findAndRegisterModules(),
                geometryService = geometryService,
            ),
            requestExpiryProperties = properties,
            transactionOperations = ImmediateTransactionOperations(),
        )
    }

    private fun overdueRequest(status: RequestStatus): RequestEntity {
        val now = LocalDateTime.now()
        return request(
            status = status,
            beginDateTime = now.minusDays(10),
            endDateTime = now.minusDays(1),
        )
    }

    private fun request(
        status: RequestStatus,
        beginDateTime: LocalDateTime,
        endDateTime: LocalDateTime,
    ): RequestEntity {
        val geometry = "POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))"
        return RequestEntity(
            id = UUID.randomUUID(),
            name = "Тест1",
            geometry = geometry,
            remainingGeometry = geometry,
            geometryArea = 1.0,
            remainingArea = 1.0,
            importance = 10.0,
            beginDateTime = beginDateTime,
            endDateTime = endDateTime,
            surveyType = RequestSurveyType.OPTICS,
            status = status,
            createdAt = beginDateTime.minusDays(1),
            updatedAt = beginDateTime.minusDays(1),
        )
    }

    // Регистрирует matcher, но возвращает non-null заглушку — иначе any() на non-null LocalDateTime
    // в Kotlin отдаёт null и ломает стек матчеров Mockito.
    private fun anyLocalDateTime(): LocalDateTime {
        any(LocalDateTime::class.java)
        return LocalDateTime.now()
    }

    private class ImmediateTransactionOperations : TransactionOperations {
        override fun <T> execute(action: TransactionCallback<T>): T {
            val status: TransactionStatus = SimpleTransactionStatus()
            return action.doInTransaction(status)
        }
    }
}
