package org.nstart.dep265.requestservice.repository

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.nstart.dep265.requestservice.PcpRequestServiceApplication
import org.nstart.dep265.requestservice.entity.OutboxEventEntity
import org.nstart.dep265.requestservice.entity.OutboxEventStatus
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.util.UUID
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

@SpringBootTest(
    classes = [PcpRequestServiceApplication::class],
    webEnvironment = SpringBootTest.WebEnvironment.NONE,
    properties = [
        "spring.config.import=",
        "spring.cloud.config.enabled=false",
        "spring.flyway.enabled=false",
        "spring.jpa.hibernate.ddl-auto=create-drop",
        "spring.datasource.url=jdbc:h2:mem:outbox-events;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false",
        "spring.datasource.driver-class-name=org.h2.Driver",
        "spring.datasource.username=sa",
        "spring.datasource.password=",
        "spring.jpa.database-platform=org.hibernate.dialect.H2Dialect",
        "spring.kafka.listener.auto-startup=false",
        "spring.kafka.consumer.group-id=request-service-test",
        "app.kafka.topics.route=request-service-test-route",
    ],
)
class OutboxEventRepositoryJpaTest @Autowired constructor(
    private val outboxEventRepository: OutboxEventRepository,
) {
    private val objectMapper = jacksonObjectMapper()

    @BeforeEach
    fun setUp() {
        outboxEventRepository.deleteAll()
    }

    @Test
    fun `unique constraint prevents duplicate request completed event for same request`() {
        val requestId = UUID.fromString("73da563f-448b-4303-a1df-41d618d3cb6d")
        outboxEventRepository.saveAndFlush(outboxEvent(requestId = requestId))

        assertFailsWith<DataIntegrityViolationException> {
            outboxEventRepository.saveAndFlush(outboxEvent(requestId = requestId))
        }
    }

    @Test
    @Transactional
    fun `publishable query returns new events before failed and excludes published events`() {
        val oldFailedEvent = outboxEvent(
            requestId = UUID.fromString("58c9fb48-3930-4c5d-8486-4385130c4706"),
            createdAt = LocalDateTime.parse("2026-01-01T09:00:00"),
            status = OutboxEventStatus.FAILED,
        )
        val newEvent = outboxEvent(
            requestId = UUID.fromString("85cb5fb8-9666-4705-bf02-1376a5d74975"),
            createdAt = LocalDateTime.parse("2026-01-01T10:00:00"),
            status = OutboxEventStatus.NEW,
        )
        val newerFailedEvent = outboxEvent(
            requestId = UUID.fromString("62faf96d-8bd5-46ba-a541-c10e78851f86"),
            createdAt = LocalDateTime.parse("2026-01-01T10:01:00"),
            status = OutboxEventStatus.FAILED,
        )
        val publishedEvent = outboxEvent(
            requestId = UUID.fromString("75051f40-9e67-4f72-9279-b4c64da409d9"),
            status = OutboxEventStatus.PUBLISHED,
        )
        val otherEventType = outboxEvent(
            requestId = UUID.fromString("d911b183-2ba5-493f-81fa-944366db63b5"),
            eventType = "REQUEST_CREATED",
            status = OutboxEventStatus.NEW,
        )
        outboxEventRepository.saveAllAndFlush(listOf(oldFailedEvent, newEvent, newerFailedEvent, publishedEvent, otherEventType))

        val events = outboxEventRepository.findPublishableForUpdateSkipLocked(
            eventType = "REQUEST_TERMINAL_STATUS",
            now = LocalDateTime.parse("2026-06-06T00:00:00"),
            maxAttempts = 10,
            batchSize = 10,
        )

        assertEquals(listOf(newEvent.id, oldFailedEvent.id, newerFailedEvent.id), events.map { it.id })
    }

    @Test
    @Transactional
    fun `publishable query excludes events whose backoff has not elapsed`() {
        val now = LocalDateTime.parse("2026-06-06T00:00:00")
        val dueFailed = outboxEvent(
            requestId = UUID.fromString("aaaaaaaa-0000-4000-8000-000000000001"),
            status = OutboxEventStatus.FAILED,
            attempts = 1,
            nextAttemptAt = now.minusMinutes(1),
        )
        val backoffPendingFailed = outboxEvent(
            requestId = UUID.fromString("bbbbbbbb-0000-4000-8000-000000000002"),
            status = OutboxEventStatus.FAILED,
            attempts = 1,
            nextAttemptAt = now.plusMinutes(5),
        )
        outboxEventRepository.saveAllAndFlush(listOf(dueFailed, backoffPendingFailed))

        val events = outboxEventRepository.findPublishableForUpdateSkipLocked(
            eventType = "REQUEST_TERMINAL_STATUS",
            now = now,
            maxAttempts = 10,
            batchSize = 10,
        )

        assertEquals(listOf(dueFailed.id), events.map { it.id })
    }

    @Test
    @Transactional
    fun `publishable query excludes events that exhausted max attempts`() {
        val now = LocalDateTime.parse("2026-06-06T00:00:00")
        val retriable = outboxEvent(
            requestId = UUID.fromString("cccccccc-0000-4000-8000-000000000003"),
            status = OutboxEventStatus.FAILED,
            attempts = 9,
        )
        val exhausted = outboxEvent(
            requestId = UUID.fromString("dddddddd-0000-4000-8000-000000000004"),
            status = OutboxEventStatus.FAILED,
            attempts = 10,
        )
        outboxEventRepository.saveAllAndFlush(listOf(retriable, exhausted))

        val events = outboxEventRepository.findPublishableForUpdateSkipLocked(
            eventType = "REQUEST_TERMINAL_STATUS",
            now = now,
            maxAttempts = 10,
            batchSize = 10,
        )

        assertEquals(listOf(retriable.id), events.map { it.id })
    }

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