package tech.easyflow.ai.easyagentsflow.repository;

import com.easyagents.flow.core.chain.runtime.Trigger;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.data.redis.core.script.RedisScript;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
 * {@link RedisTriggerStore} 分布式认领语义回归测试。
 */
public class RedisTriggerStoreTest {

    /**
     * 验证同一到期窗口超过 200 条任务时仍可一次填充本地调度容量。
     *
     * @throws Exception 测试触发器序列化失败时抛出
     */
    @Test
    @SuppressWarnings("unchecked")
    public void findDueShouldLoadMoreThanLegacyBatchLimit()
            throws Exception {
        StringRedisTemplate redisTemplate =
                Mockito.mock(StringRedisTemplate.class);
        ZSetOperations<String, String> zSetOperations =
                Mockito.mock(ZSetOperations.class);
        ValueOperations<String, String> valueOperations =
                Mockito.mock(ValueOperations.class);
        Mockito.when(redisTemplate.opsForZSet())
                .thenReturn(zSetOperations);
        Mockito.when(redisTemplate.opsForValue())
                .thenReturn(valueOperations);

        ObjectMapper objectMapper = new ObjectMapper();
        Set<String> ids = new LinkedHashSet<>();
        List<String> payloads = new ArrayList<>();
        for (int index = 0; index < 512; index++) {
            String id = "due-" + index;
            Trigger trigger = new Trigger();
            trigger.setId(id);
            trigger.setStateInstanceId(
                    "instance-" + index);
            trigger.setTriggerAt(1000L);
            ids.add(id);
            payloads.add(
                    objectMapper.writeValueAsString(
                            trigger));
        }
        Mockito.when(zSetOperations.rangeByScore(
                        ArgumentMatchers.anyString(),
                        ArgumentMatchers.anyDouble(),
                        ArgumentMatchers.anyDouble(),
                        ArgumentMatchers.eq(0L),
                        ArgumentMatchers.eq(1024L)))
                .thenReturn(ids);
        Mockito.when(valueOperations.multiGet(
                        ArgumentMatchers.anyList()))
                .thenReturn(payloads);
        RedisTriggerStore store =
                new RedisTriggerStore(
                        redisTemplate,
                        objectMapper);

        List<Trigger> due =
                store.findDue(1000L);

        Assert.assertEquals(512, due.size());
        Mockito.verify(zSetOperations)
                .rangeByScore(
                        ArgumentMatchers.anyString(),
                        ArgumentMatchers.eq(0.0),
                        ArgumentMatchers.eq(1000.0),
                        ArgumentMatchers.eq(0L),
                        ArgumentMatchers.eq(1024L));
    }

    /**
     * 验证稳定触发器通过单条 Redis 脚本完成存在性判断和创建。
     */
    @Test
    @SuppressWarnings({"unchecked", "rawtypes"})
    public void saveIfAbsentShouldUseAtomicRedisScript() {
        StringRedisTemplate redisTemplate =
                Mockito.mock(StringRedisTemplate.class);
        Mockito.doReturn(1L).when(redisTemplate).execute(
                ArgumentMatchers.<RedisScript<Long>>any(),
                ArgumentMatchers.<List<String>>any(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString());
        RedisTriggerStore store =
                new RedisTriggerStore(
                        redisTemplate,
                        new ObjectMapper());
        Trigger trigger = new Trigger();
        trigger.setId("stable-trigger");
        trigger.setTriggerAt(
                System.currentTimeMillis());

        Assert.assertTrue(
                store.saveIfAbsent(trigger));

        ArgumentCaptor<RedisScript<Long>>
                scriptCaptor =
                ArgumentCaptor.forClass(
                        (Class) RedisScript.class);
        Mockito.verify(redisTemplate).execute(
                scriptCaptor.capture(),
                ArgumentMatchers.<List<String>>any(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString());
        String script =
                scriptCaptor.getValue()
                        .getScriptAsString();
        Assert.assertTrue(script.contains(
                "exists', KEYS[1]"));
        Assert.assertTrue(script.contains(
                "psetex', KEYS[1]"));
    }

    /**
     * 验证认领触发器分配一次独立代际，并创建与该 trigger claim 绑定的执行守卫。
     *
     * <p>认领代际与实例锁 fencing token 使用不同计数器，claim 不推进实例锁 fence。</p>
     *
     * @throws Exception JSON 构造失败时抛出
     */
    @Test
    @SuppressWarnings({"unchecked", "rawtypes"})
    public void claimShouldCreateTriggerScopedExecutionGuard() throws Exception {
        StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
        ObjectMapper objectMapper = new ObjectMapper();
        Trigger stored = new Trigger();
        stored.setId("trigger-1");
        stored.setStateInstanceId("instance-1");
        stored.setTriggerAt(System.currentTimeMillis());
        stored.setFencingToken(7L);
        String payload = objectMapper.writeValueAsString(stored);
        Mockito.doReturn("8\n" + payload).when(redisTemplate).execute(
                ArgumentMatchers.<RedisScript<String>>any(),
                ArgumentMatchers.<List<String>>any(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString());
        RedisTriggerStore store =
                new RedisTriggerStore(redisTemplate, objectMapper);

        Trigger claimed = store.claim(stored, 60_000L);

        Assert.assertNotNull(claimed);
        Assert.assertEquals(8L, claimed.getFencingToken());
        ArgumentCaptor<RedisScript<String>> scriptCaptor =
                ArgumentCaptor.forClass((Class) RedisScript.class);
        ArgumentCaptor<List<String>> keysCaptor =
                ArgumentCaptor.forClass((Class) List.class);
        Mockito.verify(redisTemplate).execute(
                scriptCaptor.capture(),
                keysCaptor.capture(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString());
        Assert.assertTrue(
                scriptCaptor.getValue().getScriptAsString().contains(
                        "hset', KEYS[4], 'version'"));
        Assert.assertEquals(
                "workflowState:{instance-1}:claim:trigger-1",
                keysCaptor.getValue().get(3));
        Assert.assertEquals(
                "workflowState:{instance-1}:claim-seq",
                keysCaptor.getValue().get(4));
        Assert.assertTrue(
                scriptCaptor.getValue().getScriptAsString().contains(
                        "hincrby', KEYS[5], 'version'"));
    }
}
