package tech.easyflow.agent.runtime.agui;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;
import tech.easyflow.agent.config.AgentRuntimeProperties;
import tech.easyflow.common.web.exceptions.BusinessException;

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

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * AG-UI Redis 运行存储测试。
 */
public class AgentAguiRunStoreTest {

    /**
     * 验证 runId 原子保留、24 小时 TTL 与完成终态写入。
     */
    @Test
    public void shouldReserveRunAndPersistTerminalStatusForTwentyFourHours() {
        RedisMocks redis = redisMocks();
        AgentRuntimeProperties properties = new AgentRuntimeProperties();
        properties.setAguiRunRetention(Duration.ofHours(24));
        AgentAguiRunStore store = new AgentAguiRunStore(
                redis.template, new ObjectMapper(), properties);
        long now = System.currentTimeMillis();
        store.create(new AgentAguiRunDescriptor(
                "run-1", "request-1", "thread-1", "agent-1", "session-1",
                "user-1", "tenant-1", true, AgentAguiRunStatus.RUNNING, 0L, now, now));

        when(redis.template.execute(any(RedisScript.class), anyList(), any(Object[].class)))
                .thenReturn(2L);
        store.append("run-1", "batch-1", List.of(
                "{\"type\":\"RUN_STARTED\",\"runId\":\"run-1\",\"threadId\":\"thread-1\"}",
                "{\"type\":\"RUN_FINISHED\",\"runId\":\"run-1\",\"threadId\":\"thread-1\"}"));

        verify(redis.values).setIfAbsent(
                eq("easyflow:agent:agui:run:{run-1}:reserved"), eq("user-1"), eq(Duration.ofHours(24)));
        @SuppressWarnings("unchecked")
        ArgumentCaptor<Map<String, String>> metadata = ArgumentCaptor.forClass(Map.class);
        verify(redis.hashes).putAll(eq("easyflow:agent:agui:run:{run-1}:meta"), metadata.capture());
        Assert.assertEquals("0", metadata.getValue().get("lastCursor"));
        Assert.assertEquals("false", metadata.getValue().get("cancelRequested"));
        @SuppressWarnings("unchecked")
        ArgumentCaptor<RedisScript<Long>> script = ArgumentCaptor.forClass(RedisScript.class);
        ArgumentCaptor<Object[]> arguments = ArgumentCaptor.forClass(Object[].class);
        verify(redis.template).execute(script.capture(), anyList(), arguments.capture());
        Assert.assertTrue(script.getValue().getScriptAsString().contains("lastBatchId"));
        Assert.assertEquals("batch-1", arguments.getValue()[0]);
        Assert.assertEquals(AgentAguiRunStatus.COMPLETED.name(), arguments.getValue()[3]);
    }

    /**
     * 验证重复 runId 在模型启动前即被拒绝。
     */
    @Test(expected = BusinessException.class)
    public void shouldRejectDuplicateRunId() {
        RedisMocks redis = redisMocks();
        when(redis.values.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(false);
        AgentAguiRunStore store = new AgentAguiRunStore(
                redis.template, new ObjectMapper(), new AgentRuntimeProperties());
        store.create(new AgentAguiRunDescriptor(
                "run-1", "request-1", "thread-1", "agent-1", "session-1",
                "user-1", "tenant-1", false, AgentAguiRunStatus.RUNNING, 0L, 1L, 1L));
    }

    /**
     * 验证 owner 丢失通过同一原子批次追加明确失败终态。
     */
    @Test
    public void shouldPersistOwnerLostAsReplayableTerminalEvent() {
        RedisMocks redis = redisMocks();
        when(redis.template.execute(any(RedisScript.class), anyList(), any(Object[].class)))
                .thenReturn(1L);
        AgentAguiRunStore store = new AgentAguiRunStore(
                redis.template, new ObjectMapper(), new AgentRuntimeProperties());
        AgentAguiRunDescriptor descriptor = new AgentAguiRunDescriptor(
                "run-1", "request-1", "thread-1", "agent-1", "session-1",
                "user-1", "tenant-1", false, AgentAguiRunStatus.RUNNING, 0L, 1L, 1L);
        ArgumentCaptor<Object[]> arguments = ArgumentCaptor.forClass(Object[].class);

        store.failOwnerLost(descriptor);

        verify(redis.template).execute(any(RedisScript.class), anyList(), arguments.capture());
        Assert.assertEquals(AgentAguiRunStatus.FAILED.name(), arguments.getValue()[3]);
        Assert.assertTrue(arguments.getValue()[8].toString().contains("AGENT_RUN_OWNER_LOST"));
    }

    @SuppressWarnings("unchecked")
    private RedisMocks redisMocks() {
        StringRedisTemplate template = mock(StringRedisTemplate.class);
        ValueOperations<String, String> values = mock(ValueOperations.class);
        HashOperations<String, Object, Object> hashes = mock(HashOperations.class);
        ListOperations<String, String> lists = mock(ListOperations.class);
        when(template.opsForValue()).thenReturn(values);
        when(template.opsForHash()).thenReturn(hashes);
        when(template.opsForList()).thenReturn(lists);
        when(values.setIfAbsent(anyString(), anyString(), any(Duration.class))).thenReturn(true);
        return new RedisMocks(template, values, hashes, lists);
    }

    private record RedisMocks(
            StringRedisTemplate template,
            ValueOperations<String, String> values,
            HashOperations<String, Object, Object> hashes,
            ListOperations<String, String> lists) {
    }
}
