package org.nstart.dep265.requestservice.service

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
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.ArgumentMatchers.anyString
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoInteractions
import org.nstart.dep265.requestservice.config.OutboxProperties
import org.nstart.dep265.requestservice.entity.OutboxEventEntity
import org.nstart.dep265.requestservice.entity.OutboxEventStatus
import org.nstart.dep265.requestservice.repository.OutboxEventRepository
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.kafka.support.SendResult
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
import java.util.concurrent.CompletableFuture

class OutboxPublisherServiceTest {
    private val objectMapper = jacksonObjectMapper()
    private val properties = OutboxProperties(
        requestStatusTopic = "pcp.request.status.v1",
        publishBatchSize = 50,
        publishFixedDelayMs = 5_000,
        maxAttempts = 10,
        initialBackoffMs = 1_000,
        maxBackoffMs = 60_000,
    )

    @Test
    fun `new terminal event is published and marked as published`() {
        val requestId = UUID.fromString("6cc8ed4b-0db1-4cfb-bef0-d56eed2edce4")
        val event = outboxEvent(
            requestId = requestId,
            payload = """{"requestId":"$requestId","status":"COMPLETED","source":"outbox"}""",
        )
        val outboxEventRepository = repositoryReturning(listOf(event))
        val kafkaTemplate = kafkaTemplateReturning(successfulSend())
        val service = publisherService(outboxEventRepository, kafkaTemplate)

        val processedCount = service.publishPending()

        assertEquals(1, processedCount)
        verify(kafkaTemplate).send(
            properties.requestStatusTopic,
            requestId.toString(),
            event.payload.toString(),
        )
        assertEquals(OutboxEventStatus.PUBLISHED, event.status)
        assertNotNull(event.publishedAt)
        assertNull(event.errorMessage)
        verify(outboxEventRepository).save(event)
    }

    @Test
    fun `failed event is picked for retry`() {
        val requestId = UUID.fromString("9986e096-886a-429e-af7f-3a6178c29b4f")
        val event = outboxEvent(
            requestId = requestId,
            status = OutboxEventStatus.FAILED,
            attempts = 1,
            errorMessage = "previous failure",
        )
        val outboxEventRepository = repositoryReturning(listOf(event))
        val kafkaTemplate = kafkaTemplateReturning(successfulSend())
        val service = publisherService(outboxEventRepository, kafkaTemplate)

        service.publishPending()

        verify(kafkaTemplate).send(
            properties.requestStatusTopic,
            requestId.toString(),
            event.payload.toString(),
        )
        assertEquals(OutboxEventStatus.PUBLISHED, event.status)
        assertNull(event.errorMessage)
    }

    @Test
    fun `payload from outbox event is sent without rebuilding it`() {
        val requestId = UUID.fromString("e390c558-a09f-41c6-9e45-307c04bb3365")
        val savedPayload = """{"requestId":"$requestId","status":"COMPLETED","customField":"kept-from-outbox"}"""
        val event = outboxEvent(requestId = requestId, payload = savedPayload)
        val outboxEventRepository = repositoryReturning(listOf(event))
        val kafkaTemplate = kafkaTemplateReturning(successfulSend())
        val service = publisherService(outboxEventRepository, kafkaTemplate)

        service.publishPending()

        verify(kafkaTemplate).send(
            properties.requestStatusTopic,
            requestId.toString(),
            objectMapper.readTree(savedPayload).toString(),
        )
    }

    @Test
    fun `kafka failure increments attempts, schedules backoff and stores short error message`() {
        val requestId = UUID.fromString("116eb4c7-cd6d-4837-8525-21c1d16ecbbb")
        val event = outboxEvent(requestId = requestId)
        val outboxEventRepository = repositoryReturning(listOf(event))
        val kafkaTemplate = kafkaTemplateReturning(failedSend(IllegalStateException("broker unavailable")))
        val service = publisherService(outboxEventRepository, kafkaTemplate)

        val processedCount = service.publishPending()

        assertEquals(1, processedCount)
        assertEquals(OutboxEventStatus.FAILED, event.status)
        assertEquals(1, event.attempts)
        assertTrue(event.errorMessage!!.contains("broker unavailable"))
        assertNull(event.publishedAt)
        // backoff: следующая попытка отложена в будущее относительно создания события.
        assertTrue(event.nextAttemptAt.isAfter(event.createdAt))
        verify(outboxEventRepository).save(event)
    }

    @Test
    fun `event exhausting max attempts becomes terminal and is not rescheduled`() {
        val requestId = UUID.fromString("c0ffee00-0000-4000-8000-000000000001")
        val event = outboxEvent(
            requestId = requestId,
            status = OutboxEventStatus.FAILED,
            attempts = properties.maxAttempts - 1,
            errorMessage = "previous failure",
        )
        val outboxEventRepository = repositoryReturning(listOf(event))
        val kafkaTemplate = kafkaTemplateReturning(failedSend(IllegalStateException("still broken")))
        val service = publisherService(outboxEventRepository, kafkaTemplate)

        service.publishPending()

        assertEquals(OutboxEventStatus.FAILED, event.status)
        assertEquals(properties.maxAttempts, event.attempts)
        // Исчерпав попытки, событие терминально: backoff не назначается (next_attempt_at не двигаем).
        assertEquals(event.createdAt, event.nextAttemptAt)
    }

    @Test
    fun `failed event is not retried repeatedly in same publish pending call`() {
        val requestId = UUID.fromString("f31992a5-efae-4d9a-8262-78381da835f7")
        val event = outboxEvent(
            requestId = requestId,
            status = OutboxEventStatus.FAILED,
            attempts = 1,
            errorMessage = "previous failure",
        )
        val outboxEventRepository = repositoryReturning(listOf(event))
        val kafkaTemplate = kafkaTemplateReturning(failedSend(IllegalStateException("still broken")))
        val service = publisherService(outboxEventRepository, kafkaTemplate)

        val processedCount = service.publishPending()

        assertEquals(1, processedCount)
        verify(kafkaTemplate, times(1)).send(
            properties.requestStatusTopic,
            requestId.toString(),
            event.payload.toString(),
        )
        verify(outboxEventRepository, times(1)).findPublishableForUpdateSkipLocked(
            anyString(),
            anyLocalDateTime(),
            anyInt(),
            anyInt(),
        )
        assertEquals(OutboxEventStatus.FAILED, event.status)
        assertEquals(2, event.attempts)
        assertTrue(event.errorMessage!!.contains("still broken"))
    }

    @Test
    fun `failed event does not block other events from same batch`() {
        val failedRequestId = UUID.fromString("7000c57f-a41a-4788-8929-42a181ff2135")
        val newRequestId = UUID.fromString("e5ca9291-4d59-457c-995f-91c618dbd872")
        val failedEvent = outboxEvent(
            requestId = failedRequestId,
            status = OutboxEventStatus.FAILED,
            attempts = 1,
            errorMessage = "previous failure",
        )
        val newEvent = outboxEvent(requestId = newRequestId)
        val outboxEventRepository = repositoryReturning(listOf(newEvent, failedEvent))
        val kafkaTemplate = kafkaTemplateFailingForKeys(setOf(failedRequestId.toString()))
        val service = publisherService(outboxEventRepository, kafkaTemplate)

        val processedCount = service.publishPending()

        assertEquals(2, processedCount)
        verify(kafkaTemplate).send(
            properties.requestStatusTopic,
            failedRequestId.toString(),
            failedEvent.payload.toString(),
        )
        verify(kafkaTemplate).send(
            properties.requestStatusTopic,
            newRequestId.toString(),
            newEvent.payload.toString(),
        )
        assertEquals(OutboxEventStatus.FAILED, failedEvent.status)
        assertEquals(OutboxEventStatus.PUBLISHED, newEvent.status)
        assertNotNull(newEvent.publishedAt)
    }

    @Test
    fun `published event is not published again when repository returns no publishable rows`() {
        val outboxEventRepository = repositoryReturning(emptyList())
        val kafkaTemplate = kafkaTemplateReturning(successfulSend())
        val service = publisherService(outboxEventRepository, kafkaTemplate)

        val processedCount = service.publishPending()

        assertEquals(0, processedCount)
        verifyNoInteractions(kafkaTemplate)
    }

    private fun publisherService(
        outboxEventRepository: OutboxEventRepository,
        kafkaTemplate: KafkaTemplate<String, String>,
    ): OutboxPublisherService {
        return OutboxPublisherService(
            outboxEventRepository = outboxEventRepository,
            kafkaTemplate = kafkaTemplate,
            outboxProperties = properties,
            transactionOperations = ImmediateTransactionOperations(),
        )
    }

    private fun repositoryReturning(events: List<OutboxEventEntity>): OutboxEventRepository {
        val outboxEventRepository = mock(OutboxEventRepository::class.java)
        doReturn(events)
            .`when`(outboxEventRepository)
            .findPublishableForUpdateSkipLocked(
                anyString(),
                anyLocalDateTime(),
                anyInt(),
                anyInt(),
            )
        return outboxEventRepository
    }

    @Suppress("UNCHECKED_CAST")
    private fun kafkaTemplateReturning(
        result: CompletableFuture<SendResult<String, String>>,
    ): KafkaTemplate<String, String> {
        val kafkaTemplate = mock(KafkaTemplate::class.java) as KafkaTemplate<String, String>
        doReturn(result)
            .`when`(kafkaTemplate)
            .send(anyString(), anyString(), anyString())
        return kafkaTemplate
    }

    @Suppress("UNCHECKED_CAST")
    private fun kafkaTemplateFailingForKeys(failedKeys: Set<String>): KafkaTemplate<String, String> {
        val kafkaTemplate = mock(KafkaTemplate::class.java) as KafkaTemplate<String, String>
        doAnswer { invocation ->
            val key = invocation.arguments[1] as String
            if (key in failedKeys) {
                failedSend(IllegalStateException("broker unavailable for $key"))
            } else {
                successfulSend()
            }
        }
            .`when`(kafkaTemplate)
            .send(anyString(), anyString(), anyString())
        return kafkaTemplate
    }

    @Suppress("UNCHECKED_CAST")
    private fun successfulSend(): CompletableFuture<SendResult<String, String>> {
        val sendResult = mock(SendResult::class.java) as SendResult<String, String>
        return CompletableFuture.completedFuture(sendResult)
    }

    private fun failedSend(exception: Exception): CompletableFuture<SendResult<String, String>> {
        val future = CompletableFuture<SendResult<String, String>>()
        future.completeExceptionally(exception)
        return future
    }

    private fun outboxEvent(
        requestId: UUID,
        payload: String = """{"requestId":"$requestId","status":"COMPLETED"}""",
        status: OutboxEventStatus = OutboxEventStatus.NEW,
        attempts: Int = 0,
        errorMessage: String? = null,
    ): OutboxEventEntity {
        val createdAt = LocalDateTime.parse("2026-01-01T10:00:00")
        return OutboxEventEntity(
            id = UUID.randomUUID(),
            eventType = RequestStatusOutboxService.REQUEST_STATUS_EVENT_TYPE,
            aggregateType = RequestStatusOutboxService.REQUEST_AGGREGATE_TYPE,
            aggregateId = requestId,
            payload = objectMapper.readTree(payload),
            status = status,
            createdAt = createdAt,
            attempts = attempts,
            nextAttemptAt = createdAt,
            errorMessage = errorMessage,
        )
    }

    // Регистрирует 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 : Any?> execute(action: TransactionCallback<T>): T {
            return action.doInTransaction(SimpleTransactionStatus())
        }

        override fun executeWithoutResult(action: java.util.function.Consumer<TransactionStatus>) {
            action.accept(SimpleTransactionStatus())
        }
    }
}
