package org.nstart.dep265.requestservice.config

import jakarta.validation.ConstraintViolation
import jakarta.validation.ConstraintViolationException
import org.apache.kafka.clients.consumer.Consumer
import org.apache.kafka.clients.consumer.ConsumerRecord
import org.apache.kafka.clients.producer.ProducerRecord
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.mock
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.springframework.boot.autoconfigure.AutoConfigurations
import org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.test.context.runner.ApplicationContextRunner
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.kafka.core.KafkaTemplate
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer
import org.springframework.kafka.listener.DefaultErrorHandler
import org.springframework.kafka.listener.MessageListenerContainer
import org.springframework.kafka.support.SendResult
import java.nio.charset.StandardCharsets
import java.util.concurrent.CompletableFuture

class KafkaConsumerConfigTest {

    private val contextRunner = ApplicationContextRunner()
        .withConfiguration(AutoConfigurations.of(ConfigurationPropertiesAutoConfiguration::class.java))
        .withUserConfiguration(TestConfiguration::class.java, KafkaConsumerConfig::class.java)
        .withPropertyValues(
            "spring.kafka.bootstrap-servers=localhost:9092",
            "spring.kafka.consumer.group-id=pcp-request-service",
            "spring.kafka.consumer.auto-offset-reset=earliest",
            "app.kafka.consumer.route.dlq-topic=$DLQ_TOPIC",
            "app.kafka.consumer.route.max-attempts=3",
            "app.kafka.consumer.route.backoff-ms=0",
            "app.kafka.consumer.route.max-backoff-ms=0",
        )

    @Test
    fun `creates error handler and recoverer beans`() {
        contextRunner.run { context ->
            assertNotNull(context.getBean(DefaultErrorHandler::class.java))
            assertNotNull(context.getBean(DeadLetterPublishingRecoverer::class.java))

            val properties = context.getBean(RouteConsumerProperties::class.java)
            assertEquals(DLQ_TOPIC, properties.dlqTopic)
            assertEquals(3L, properties.maxAttempts)
        }
    }

    @Test
    fun `recoverer publishes original record to configured dlq topic with diagnostics headers`() {
        contextRunner.run { context ->
            val kafkaTemplate = context.kafkaTemplate()
            val recoverer = context.getBean(DeadLetterPublishingRecoverer::class.java)
            val record = consumerRecord()

            recoverer.accept(record, ConstraintViolationException("route payload invalid", emptySet<ConstraintViolation<*>>()))

            val published = capturePublishedRecord(kafkaTemplate)
            assertEquals(DLQ_TOPIC, published.topic())
            assertEquals(record.partition(), published.partition())
            assertEquals(record.key(), published.key())
            assertEquals(record.value(), published.value())
            assertEquals("pcp-request-service", published.headers().stringHeader("pcp-service-name"))
            assertEquals(
                ConstraintViolationException::class.java.name,
                published.headers().stringHeader("pcp-exception-class"),
            )
            assertEquals("route payload invalid", published.headers().stringHeader("pcp-exception-message"))
            assertEquals(ROUTE_TOPIC, published.headers().stringHeader("pcp-original-topic"))
            assertEquals("3", published.headers().stringHeader("pcp-original-partition"))
            assertEquals("42", published.headers().stringHeader("pcp-original-offset"))
        }
    }

    @Test
    fun `contract violation is recovered to dlq without retry`() {
        contextRunner.run { context ->
            val kafkaTemplate = context.kafkaTemplate()
            val errorHandler = context.getBean(DefaultErrorHandler::class.java)

            val handled = errorHandler.handleOne(
                ConstraintViolationException("route payload invalid", emptySet<ConstraintViolation<*>>()),
                consumerRecord(),
                mockConsumer(),
                mock(MessageListenerContainer::class.java),
            )

            assertTrue(handled)
            verify(kafkaTemplate, times(1)).send(anyProducerRecord())
        }
    }

    @Test
    fun `transient exception is retried with bounded policy before dlq recovery`() {
        contextRunner.run { context ->
            val kafkaTemplate = context.kafkaTemplate()
            val errorHandler = context.getBean(DefaultErrorHandler::class.java)
            val record = consumerRecord()
            val consumer = mockConsumer()
            val container = mock(MessageListenerContainer::class.java)
            val exception = IllegalStateException("database is temporarily unavailable")

            // maxAttempts=3 → две первые доставки ретраятся (seek), на третьей — DLQ.
            assertFalse(errorHandler.handleOne(exception, record, consumer, container))
            assertFalse(errorHandler.handleOne(exception, record, consumer, container))
            verify(kafkaTemplate, never()).send(anyProducerRecord())

            assertTrue(errorHandler.handleOne(exception, record, consumer, container))
            verify(kafkaTemplate, times(1)).send(anyProducerRecord())
        }
    }

    private fun consumerRecord(): ConsumerRecord<String, String> =
        ConsumerRecord(ROUTE_TOPIC, 3, 42L, "route-key", """{"routeId":"route-1"}""")

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

    @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 mockConsumer(): Consumer<String, String> =
        mock(Consumer::class.java) as Consumer<String, String>

    private fun org.apache.kafka.common.header.Headers.stringHeader(name: String): String? =
        lastHeader(name)?.value()?.toString(StandardCharsets.UTF_8)

    @Configuration(proxyBeanMethods = false)
    @EnableConfigurationProperties(RouteConsumerProperties::class)
    private class TestConfiguration {

        @Bean
        @Suppress("UNCHECKED_CAST")
        fun kafkaTemplate(): KafkaTemplate<String, String> {
            val kafkaTemplate = mock(KafkaTemplate::class.java) as KafkaTemplate<String, String>
            val sendResult = mock(SendResult::class.java) as SendResult<String, String>
            `when`(kafkaTemplate.send(anyProducerRecord()))
                .thenReturn(CompletableFuture.completedFuture(sendResult))
            return kafkaTemplate
        }
    }

    private companion object {
        const val ROUTE_TOPIC = "pcp.route.georeference.v1"
        const val DLQ_TOPIC = "pcp.route.georeference-dlq.v1"

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

        @Suppress("UNCHECKED_CAST")
        fun org.springframework.context.ApplicationContext.kafkaTemplate(): KafkaTemplate<String, String> =
            getBean(KafkaTemplate::class.java) as KafkaTemplate<String, String>
    }
}
