package space.nstart.pcp.routepassportconsumer.service

import org.junit.jupiter.api.Test
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.doThrow
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
import space.nstart.pcp.routepassportconsumer.config.RoutePassportOutboxProperties
import space.nstart.pcp.routepassportconsumer.entity.RoutePassportOutboxEntity
import space.nstart.pcp.routepassportconsumer.entity.RoutePassportOutboxStatus
import com.fasterxml.jackson.databind.ObjectMapper
import java.time.LocalDateTime
import java.util.UUID

class RoutePassportOutboxWorkerTest {

    private val objectMapper = ObjectMapper().findAndRegisterModules()

    @Test
    fun `when disabled no claim or publish happens`() {
        val outboxService = mock(RoutePassportOutboxService::class.java)
        val publisherService = mock(RoutePassportKafkaPublisherService::class.java)
        val worker = worker(
            outboxService = outboxService,
            publisherService = publisherService,
            properties = properties(enabled = false),
        )

        worker.publishDueOutboxRows()

        verify(outboxService, never()).claimDueBatch()
        verify(publisherService, never()).publish(anyOutboxEntity())
    }

    @Test
    fun `when claim returns rows publisher is called for each row`() {
        val outboxService = mock(RoutePassportOutboxService::class.java)
        val publisherService = mock(RoutePassportKafkaPublisherService::class.java)
        val first = outbox(id = UUID.fromString("3ed5e75c-a5c0-4d63-88be-a701b5222b62"))
        val second = outbox(id = UUID.fromString("5d75d482-e402-46d4-8e2b-4c19bff6f795"))
        val worker = worker(outboxService, publisherService)

        doReturn(listOf(first, second))
            .`when`(outboxService)
            .claimDueBatch()

        worker.publishDueOutboxRows()

        verify(publisherService).publish(first)
        verify(publisherService).publish(second)
    }

    @Test
    fun `successful publish marks row as published`() {
        val outboxService = mock(RoutePassportOutboxService::class.java)
        val publisherService = mock(RoutePassportKafkaPublisherService::class.java)
        val outbox = outbox()
        val worker = worker(outboxService, publisherService)

        doReturn(listOf(outbox))
            .`when`(outboxService)
            .claimDueBatch()

        worker.publishDueOutboxRows()

        verify(publisherService).publish(outbox)
        verify(outboxService).markPublished(outbox.id)
    }

    @Test
    fun `publish failure marks row retry or failed`() {
        val outboxService = mock(RoutePassportOutboxService::class.java)
        val publisherService = mock(RoutePassportKafkaPublisherService::class.java)
        val outbox = outbox()
        val failure = IllegalStateException("broker unavailable")
        val worker = worker(outboxService, publisherService)

        doReturn(listOf(outbox))
            .`when`(outboxService)
            .claimDueBatch()
        doThrow(failure)
            .`when`(publisherService)
            .publish(outbox)

        worker.publishDueOutboxRows()

        verify(outboxService).markRetryOrFailed(outbox.id, failure)
        verify(outboxService, never()).markPublished(outbox.id)
    }

    @Test
    fun `failure of one row does not prevent processing next row`() {
        val outboxService = mock(RoutePassportOutboxService::class.java)
        val publisherService = mock(RoutePassportKafkaPublisherService::class.java)
        val first = outbox(id = UUID.fromString("3ed5e75c-a5c0-4d63-88be-a701b5222b62"))
        val second = outbox(id = UUID.fromString("5d75d482-e402-46d4-8e2b-4c19bff6f795"))
        val failure = IllegalStateException("broker unavailable")
        val worker = worker(outboxService, publisherService)

        doReturn(listOf(first, second))
            .`when`(outboxService)
            .claimDueBatch()
        doThrow(failure)
            .`when`(publisherService)
            .publish(first)

        worker.publishDueOutboxRows()

        verify(outboxService).markRetryOrFailed(first.id, failure)
        verify(publisherService).publish(second)
        verify(outboxService).markPublished(second.id)
    }

    @Test
    fun `row status transition failure does not prevent processing next row`() {
        val outboxService = mock(RoutePassportOutboxService::class.java)
        val publisherService = mock(RoutePassportKafkaPublisherService::class.java)
        val first = outbox(id = UUID.fromString("3ed5e75c-a5c0-4d63-88be-a701b5222b62"))
        val second = outbox(id = UUID.fromString("5d75d482-e402-46d4-8e2b-4c19bff6f795"))
        val publishFailure = IllegalStateException("broker unavailable")
        val transitionFailure = IllegalStateException("database unavailable")
        val worker = worker(outboxService, publisherService)

        doReturn(listOf(first, second))
            .`when`(outboxService)
            .claimDueBatch()
        doThrow(publishFailure)
            .`when`(publisherService)
            .publish(first)
        doThrow(transitionFailure)
            .`when`(outboxService)
            .markRetryOrFailed(first.id, publishFailure)

        worker.publishDueOutboxRows()

        verify(publisherService).publish(second)
        verify(outboxService).markPublished(second.id)
    }

    private fun worker(
        outboxService: RoutePassportOutboxService,
        publisherService: RoutePassportKafkaPublisherService,
        properties: RoutePassportOutboxProperties = properties(),
    ): RoutePassportOutboxWorker =
        RoutePassportOutboxWorker(
            routePassportOutboxService = outboxService,
            routePassportKafkaPublisherService = publisherService,
            outboxProperties = properties,
        )

    private fun properties(enabled: Boolean = true): RoutePassportOutboxProperties =
        RoutePassportOutboxProperties(
            enabled = enabled,
            batchSize = 50,
            fixedDelayMs = 1_000,
            lockTimeoutMs = 60_000,
            initialBackoffMs = 1_000,
            maxBackoffMs = 60_000,
        )

    private fun outbox(
        id: UUID = UUID.fromString("3ed5e75c-a5c0-4d63-88be-a701b5222b62"),
    ): RoutePassportOutboxEntity {
        val routeId = UUID.fromString("51b07d12-1823-4bd5-b55d-de02a5304c03")
        val now = LocalDateTime.of(2026, 4, 1, 10, 15, 0)

        return RoutePassportOutboxEntity(
            id = id,
            routeId = routeId,
            eventType = PcpKafkaEvent.ModeStatusChangedEvent.name,
            topic = "pcp.route.in.v1",
            messageKey = routeId.toString(),
            payload = objectMapper.readTree("""{"type":"ModeStatusChangedEvent","data":{"routeNameFull":"ROUTE-001"}}"""),
            headers = null,
            status = RoutePassportOutboxStatus.PENDING,
            attempts = 0,
            maxAttempts = 10,
            nextAttemptAt = now,
            lockedUntil = now.plusMinutes(1),
            lastError = null,
            createdAt = now,
            updatedAt = now,
        )
    }

    private fun anyOutboxEntity(): RoutePassportOutboxEntity =
        org.mockito.ArgumentMatchers.any(RoutePassportOutboxEntity::class.java) ?: outbox()
}
