package tech.easyflow.common.cache;

import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;

import java.lang.reflect.Constructor;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * {@link RedisIdempotencyExecutor} 状态转换回归测试。
 */
public class RedisIdempotencyExecutorTest {

    /**
     * 验证存在测试构造器时生产构造器仍能被 Spring 明确选择。
     *
     * @throws Exception 生产构造器不存在时抛出
     */
    @Test
    public void productionConstructorShouldBeAutowired()
            throws Exception {
        Constructor<RedisIdempotencyExecutor> constructor =
                RedisIdempotencyExecutor.class.getConstructor(
                        StringRedisTemplate.class);

        Assert.assertNotNull(
                constructor.getAnnotation(Autowired.class));
    }

    /**
     * 验证首次认领会执行操作并写入完成凭证。
     */
    @Test
    public void executeOnceShouldRunAndCompleteForNewKey() {
        StringRedisTemplate redisTemplate = redisTemplateReturning(1L, 1L);
        RedisIdempotencyExecutor executor = new RedisIdempotencyExecutor(redisTemplate);
        AtomicInteger executions = new AtomicInteger();

        boolean executed = executor.executeOnce(
                "workflow:instance:node:trigger", executions::incrementAndGet);

        Assert.assertTrue(executed);
        Assert.assertEquals(1, executions.get());
        Mockito.verify(redisTemplate).execute(
                ArgumentMatchers.<RedisScript<Long>>any(),
                ArgumentMatchers.<List<String>>any(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString());
        Mockito.verify(redisTemplate).execute(
                ArgumentMatchers.<RedisScript<Long>>any(),
                ArgumentMatchers.<List<String>>any(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString());
    }

    /**
     * 验证已有完成凭证时直接跳过副作用操作。
     */
    @Test
    public void executeOnceShouldSkipCompletedKey() {
        StringRedisTemplate redisTemplate = redisTemplateReturning(2L);
        RedisIdempotencyExecutor executor = new RedisIdempotencyExecutor(redisTemplate);
        AtomicInteger executions = new AtomicInteger();

        boolean executed = executor.executeOnce(
                "workflow:instance:node:trigger", executions::incrementAndGet);

        Assert.assertFalse(executed);
        Assert.assertEquals(0, executions.get());
    }

    /**
     * 验证其他 owner 仍在处理时返回明确冲突。
     */
    @Test
    public void executeOnceShouldRejectInProgressKey() {
        StringRedisTemplate redisTemplate = redisTemplateReturning(0L);
        RedisIdempotencyExecutor executor = new RedisIdempotencyExecutor(redisTemplate);

        try {
            executor.executeOnce("workflow:instance:node:trigger", () -> {
            });
            Assert.fail("in-progress operation must be rejected");
        } catch (RedisIdempotencyExecutor.IdempotentOperationInProgressException expected) {
            Assert.assertTrue(expected.getMessage().contains("workflow:instance"));
        }
    }

    /**
     * 验证同一幂等键绑定不同负载时明确拒绝，避免把不同业务写入误判为已完成。
     */
    @Test
    public void executeOnceShouldRejectPayloadMismatch() {
        StringRedisTemplate redisTemplate = redisTemplateReturning(-1L);
        RedisIdempotencyExecutor executor = new RedisIdempotencyExecutor(redisTemplate);

        try {
            executor.executeOnce(
                    "workflow:instance:node:trigger",
                    "different-payload-hash",
                    () -> {
                    });
            Assert.fail("payload mismatch must be rejected");
        } catch (RedisIdempotencyExecutor.IdempotencyPayloadMismatchException expected) {
            Assert.assertTrue(expected.getMessage().contains("workflow:instance"));
        }
    }

    /**
     * 验证长操作会在处理中凭证到期前持续续期。
     *
     * @throws Exception 测试等待被中断
     */
    @Test
    public void executeOnceShouldRenewLeaseForLongOperation()
            throws Exception {
        StringRedisTemplate redisTemplate =
                redisTemplateReturning(1L, 1L);
        Mockito.when(redisTemplate.execute(
                ArgumentMatchers.<RedisScript<Long>>any(),
                ArgumentMatchers.<List<String>>any(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString()))
                .thenReturn(1L);
        RedisIdempotencyExecutor executor =
                new RedisIdempotencyExecutor(
                        redisTemplate,
                        Duration.ofMillis(60L),
                        Duration.ofMinutes(1L));

        Assert.assertTrue(executor.executeOnce(
                "workflow:long-operation",
                () -> {
                    try {
                        Thread.sleep(90L);
                    } catch (InterruptedException exception) {
                        Thread.currentThread().interrupt();
                        throw new IllegalStateException(
                                "test interrupted", exception);
                    }
                }));

        Mockito.verify(redisTemplate, Mockito.atLeastOnce()).execute(
                ArgumentMatchers.<RedisScript<Long>>any(),
                ArgumentMatchers.<List<String>>any(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString());
    }

    /**
     * 创建按顺序返回脚本结果的 Redis 模板。
     *
     * @param results 脚本返回值
     * @return Redis 模板
     */
    private StringRedisTemplate redisTemplateReturning(Long... results) {
        StringRedisTemplate redisTemplate = Mockito.mock(StringRedisTemplate.class);
        Mockito.when(redisTemplate.execute(
                ArgumentMatchers.<RedisScript<Long>>any(),
                ArgumentMatchers.<List<String>>any(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString()
        )).thenReturn(results[0]);
        Mockito.when(redisTemplate.execute(
                ArgumentMatchers.<RedisScript<Long>>any(),
                ArgumentMatchers.<List<String>>any(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString()
        )).thenReturn(results[Math.min(1, results.length - 1)]);
        return redisTemplate;
    }
}
