package tech.easyflow.common.mq.redis;

import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.stream.Consumer;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.PendingMessage;
import org.springframework.data.redis.connection.stream.PendingMessages;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import tech.easyflow.common.mq.config.MQProperties;
import tech.easyflow.common.mq.core.MQConsumerHandler;
import tech.easyflow.common.mq.core.MQDeadLetterService;
import tech.easyflow.common.mq.core.MQMessage;
import tech.easyflow.common.mq.core.MQMessageConverter;
import tech.easyflow.common.mq.core.MQSubscription;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * {@link RedisMQConsumerContainer} 回归测试。
 */
public class RedisMQConsumerContainerTest {

    /**
     * 验证 consumer name 包含稳定实例 ID，且消费组名称不被改变。
     */
    @Test
    public void buildConsumerNameShouldAppendSanitizedInstanceId() {
        MQProperties properties = new MQProperties();
        properties.getRedis().setConsumerInstanceId("node/a:1");
        RedisMQConsumerContainer container = new RedisMQConsumerContainer(
            null,
            null,
            properties,
            null,
            null,
            null,
            List.of()
        );

        String consumerName = container.buildConsumerName("chat-persist", 2);

        Assert.assertEquals("chat-persist-2-node-a-1", consumerName);
    }

    /**
     * 验证关闭批量消费后，容器按单条处理并独立确认消息。
     *
     * @throws Exception 消息处理异常
     */
    @Test
    public void handleMessagesShouldProcessIndividuallyWhenBatchDisabled() throws Exception {
        StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
        @SuppressWarnings("unchecked")
        StreamOperations<String, Object, Object> streamOperations = Mockito.mock(StreamOperations.class);
        Mockito.when(redisTemplate.opsForStream()).thenReturn(streamOperations);
        RecordingHandler handler = new RecordingHandler();
        MQSubscription subscription = new MQSubscription();
        subscription.setBatchEnabled(false);
        RedisMQConsumerContainer container = container(redisTemplate, null);
        MQMessage first = message("message-1", "1-0");
        MQMessage second = message("message-2", "2-0");

        container.handleMessages(handler, subscription, "stream-1", "group-1", List.of(first, second));

        Assert.assertEquals(List.of(List.of("message-1"), List.of("message-2")), handler.calls);
        Mockito.verify(streamOperations).acknowledge("stream-1", "group-1", "1-0");
        Mockito.verify(streamOperations).acknowledge("stream-1", "group-1", "2-0");
    }

    /**
     * 验证 pending 消息被 claim 后可以转换为 MQ 消息继续消费。
     */
    @Test
    public void reclaimPendingShouldReturnClaimedRecordsForConsumption() {
        StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
        @SuppressWarnings("unchecked")
        StreamOperations<String, Object, Object> streamOperations = Mockito.mock(StreamOperations.class);
        Mockito.when(redisTemplate.opsForStream()).thenReturn(streamOperations);
        RedisConnectionFactory connectionFactory = Mockito.mock(RedisConnectionFactory.class);
        RedisConnection connection = Mockito.mock(RedisConnection.class);
        RedisStreamCommands streamCommands = Mockito.mock(RedisStreamCommands.class);
        Mockito.when(connectionFactory.getConnection()).thenReturn(connection);
        Mockito.when(connection.streamCommands()).thenReturn(streamCommands);
        PendingMessage pendingMessage = new PendingMessage(
                RecordId.of("1-0"), Consumer.from("group-1", "old-consumer"), Duration.ofMinutes(2), 1);
        Mockito.when(streamCommands.xPending(
                        ArgumentMatchers.eq("stream-1".getBytes(java.nio.charset.StandardCharsets.UTF_8)),
                        ArgumentMatchers.eq("group-1"),
                        ArgumentMatchers.any(RedisStreamCommands.XPendingOptions.class)))
                .thenReturn(new PendingMessages("group-1", List.of(pendingMessage)));
        Map<Object, Object> payload = Map.of("payload", "message-1");
        MapRecord<String, Object, Object> record = MapRecord
                .create("stream-1", payload)
                .withId(RecordId.of("1-0"));
        Mockito.when(streamOperations.claim(
                        ArgumentMatchers.eq("stream-1"),
                        ArgumentMatchers.eq("group-1"),
                        ArgumentMatchers.eq("consumer-1"),
                        ArgumentMatchers.any(Duration.class),
                        ArgumentMatchers.any(RecordId[].class)))
                .thenReturn(List.of(record));
        RedisMQConsumerContainer container = container(redisTemplate, connectionFactory);

        List<MapRecord<String, Object, Object>> records =
                container.reclaimPending("stream-1", "group-1", "consumer-1");
        List<MQMessage> messages = container.toMessages("stream-1", records);

        Assert.assertEquals(1, records.size());
        Assert.assertEquals(1, messages.size());
        Assert.assertEquals("message-1", messages.get(0).getMessageId());
        Assert.assertEquals("1-0", messages.get(0).getStreamMessageId());
    }

    private RedisMQConsumerContainer container(StringRedisTemplate redisTemplate,
                                              RedisConnectionFactory connectionFactory) {
        MQProperties properties = new MQProperties();
        return new RedisMQConsumerContainer(
                connectionFactory,
                redisTemplate,
                properties,
                new PlainMessageConverter(),
                Mockito.mock(MQDeadLetterService.class),
                null,
                List.of()
        );
    }

    private MQMessage message(String messageId, String streamMessageId) {
        MQMessage message = new MQMessage();
        message.setMessageId(messageId);
        message.setStreamMessageId(streamMessageId);
        return message;
    }

    private static final class RecordingHandler implements MQConsumerHandler {

        private final List<List<String>> calls = new ArrayList<>();

        @Override
        public MQSubscription subscription() {
            return new MQSubscription();
        }

        @Override
        public void handle(List<MQMessage> messages) {
            calls.add(messages.stream().map(MQMessage::getMessageId).toList());
        }
    }

    private static final class PlainMessageConverter implements MQMessageConverter {

        @Override
        public String serialize(MQMessage message) {
            return message.getMessageId();
        }

        @Override
        public MQMessage deserialize(String payload) {
            MQMessage message = new MQMessage();
            message.setMessageId(payload);
            return message;
        }
    }
}
