package space.nstart.pcp.routepassportconsumer.service

import org.apache.kafka.clients.producer.ProducerRecord
import org.apache.kafka.clients.producer.RecordMetadata
import org.apache.kafka.common.TopicPartition
import org.junit.jupiter.api.Test
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.kafka.support.SendResult
import space.nstart.pcp.pcp_types_lib.message.PcpKafkaEvent
import space.nstart.pcp.routepassportconsumer.entity.RoutePassportOutboxEntity
import space.nstart.pcp.routepassportconsumer.entity.RoutePassportOutboxStatus
import com.fasterxml.jackson.databind.ObjectMapper
import java.nio.charset.StandardCharsets
import java.time.LocalDateTime
import java.util.UUID
import java.util.concurrent.CompletableFuture
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertSame
import kotlin.test.assertTrue
import kotlin.test.assertFailsWith

class RoutePassportKafkaPublisherServiceTest {

    private val objectMapper = ObjectMapper().findAndRegisterModules()

    @Test
    fun `successful publish sends one outbox record with topic key payload and type header`() {
        val kafkaTemplate = kafkaTemplate()
        val publisherService = publisherService(kafkaTemplate)
        val outbox = outbox()

        doAnswer { invocation ->
            @Suppress("UNCHECKED_CAST")
            val record = invocation.arguments[0] as ProducerRecord<String, String>
            CompletableFuture.completedFuture(sendResult(record))
        }.`when`(kafkaTemplate).send(anyProducerRecord())

        publisherService.publish(outbox)

        val record = captureSingleRecord(kafkaTemplate)
        assertEquals(outbox.topic, record.topic())
        assertEquals(outbox.messageKey, record.key())
        assertEquals(objectMapper.writeValueAsString(outbox.payload), record.value())
        assertEquals(
            outbox.eventType,
            record.headers().lastHeader("type").value().toString(StandardCharsets.UTF_8),
        )
    }

    @Test
    fun `send failure is propagated to caller`() {
        val kafkaTemplate = kafkaTemplate()
        val publisherService = publisherService(kafkaTemplate)
        val outbox = outbox()
        val failure = IllegalStateException("broker unavailable")
        val failedFuture = CompletableFuture<SendResult<String, String>>()
            .apply { completeExceptionally(failure) }

        doReturn(failedFuture)
            .`when`(kafkaTemplate)
            .send(anyProducerRecord())

        val thrown = assertFailsWith<IllegalStateException> {
            publisherService.publish(outbox)
        }

        assertSame(failure, thrown)
        verify(kafkaTemplate, times(1)).send(anyProducerRecord())
    }

    @Test
    fun `payload JsonNode is serialized as JSON object instead of escaped JSON string`() {
        val kafkaTemplate = kafkaTemplate()
        val publisherService = publisherService(kafkaTemplate)
        val outbox = outbox(
            payload = """{"type":"ModeStatusChangedEvent","data":{"routeId":"51b07d12-1823-4bd5-b55d-de02a5304c03"}}""",
        )

        doAnswer { invocation ->
            @Suppress("UNCHECKED_CAST")
            val record = invocation.arguments[0] as ProducerRecord<String, String>
            CompletableFuture.completedFuture(sendResult(record))
        }.`when`(kafkaTemplate).send(anyProducerRecord())

        publisherService.publish(outbox)

        val sentPayload = captureSingleRecord(kafkaTemplate).value()
        assertFalse(sentPayload.startsWith("\""))
        assertTrue(objectMapper.readTree(sentPayload).isObject)
        assertEquals(
            PcpKafkaEvent.ModeStatusChangedEvent.name,
            objectMapper.readTree(sentPayload).path("type").asText(),
        )
    }

    private fun publisherService(
        kafkaTemplate: KafkaTemplate<String, String>,
    ): RoutePassportKafkaPublisherService =
        RoutePassportKafkaPublisherService(
            kafkaTemplate = kafkaTemplate,
        )

    @Suppress("UNCHECKED_CAST")
    private fun kafkaTemplate(): KafkaTemplate<String, String> =
        mock(KafkaTemplate::class.java) as KafkaTemplate<String, String>

    private fun captureSingleRecord(
        kafkaTemplate: KafkaTemplate<String, String>,
    ): ProducerRecord<String, String> {
        val captor = producerRecordCaptor()
        verify(kafkaTemplate, times(1)).send(captor.capture())
        return captor.value
    }

    private fun sendResult(record: ProducerRecord<String, String>): SendResult<String, String> =
        SendResult(
            record,
            RecordMetadata(
                TopicPartition(record.topic(), 2),
                42L,
                0,
                0L,
                record.key()?.length ?: -1,
                record.value()?.length ?: -1,
            ),
        )

    private fun outbox(
        payload: String = """{"type":"ModeStatusChangedEvent","data":{"routeNameFull":"ROUTE-001"}}""",
    ): RoutePassportOutboxEntity {
        val routeId = UUID.fromString("51b07d12-1823-4bd5-b55d-de02a5304c03")
        val now = LocalDateTime.of(2026, 4, 1, 10, 15, 0)

        return RoutePassportOutboxEntity(
            id = UUID.fromString("3ed5e75c-a5c0-4d63-88be-a701b5222b62"),
            routeId = routeId,
            eventType = PcpKafkaEvent.ModeStatusChangedEvent.name,
            topic = "pcp.route.in.v1",
            messageKey = routeId.toString(),
            payload = objectMapper.readTree(payload),
            headers = null,
            status = RoutePassportOutboxStatus.PENDING,
            attempts = 0,
            maxAttempts = 10,
            nextAttemptAt = now,
            createdAt = now,
            updatedAt = now,
        )
    }

    @Suppress("UNCHECKED_CAST")
    private fun producerRecordCaptor(): ArgumentCaptor<ProducerRecord<String, String>> =
        ArgumentCaptor.forClass(ProducerRecord::class.java) as ArgumentCaptor<ProducerRecord<String, String>>

    @Suppress("UNCHECKED_CAST")
    private fun anyProducerRecord(): ProducerRecord<String, String> =
        (any(ProducerRecord::class.java) as ProducerRecord<String, String>?)
            ?: ProducerRecord("stub-topic", "stub-key", "stub-payload")
}
