package tech.easyflow.ai.easyagentsflow.repository;

import com.alicp.jetcache.Cache;
import com.alicp.jetcache.CacheException;
import com.alicp.jetcache.CacheGetResult;
import com.alicp.jetcache.CacheResult;
import com.alicp.jetcache.CacheResultCode;
import com.alicp.jetcache.CacheValueHolder;
import com.alicp.jetcache.support.CacheEncodeException;
import com.easyagents.flow.core.chain.ChainState;
import com.easyagents.flow.core.chain.ChainStatus;
import com.easyagents.flow.core.chain.repository.ChainLock;
import com.easyagents.flow.core.chain.repository.ChainStateField;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.common.cache.VersionedObjectStore;
import tech.easyflow.common.cache.VersionedFields;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.time.Duration;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentHashMap;

/**
 * {@link ChainStateRepositoryImpl} 缓存迁移和版本提交回归测试。
 */
public class ChainStateRepositoryImplTest {

    /**
     * 验证旧缓存解码失败时抛出异常且不创建空工作流状态。
     *
     * @throws Exception 测试依赖注入失败时抛出
     */
    @Test
    public void loadShouldFailWithoutOverwritingStateWhenCacheDecodeFails() throws Exception {
        String instanceId = "decode-failed-instance";
        CacheGetResult<Object> failure = new CacheGetResult<>(new CacheEncodeException(
                "decode error",
                new ClassNotFoundException("com.alicp.jetcache.CacheValueHolder")
        ));
        RecordingCache cache = new RecordingCache(failure);
        RecordingVersionedObjectStore stateStore = new RecordingVersionedObjectStore();
        ChainStateRepositoryImpl repository = repository(cache.asCache(), stateStore);

        try {
            repository.load(instanceId);
            Assert.fail("cache decode failure should be propagated");
        } catch (CacheException expected) {
            Assert.assertTrue(expected.getMessage().contains("工作流状态缓存读取失败"));
            Assert.assertTrue(expected.getMessage().contains(instanceId));
        }

        Assert.assertEquals(0, stateStore.getCreateCount());
    }

    /**
     * 验证新实例通过版本对象存储显式创建。
     *
     * @throws Exception 测试依赖注入失败时抛出
     */
    @Test
    public void createShouldPersistStateWhenStateDoesNotExist() throws Exception {
        String instanceId = "new-instance";
        RecordingCache cache = new RecordingCache(
                new CacheGetResult<>(CacheResultCode.NOT_EXISTS, null, null));
        RecordingVersionedObjectStore stateStore = new RecordingVersionedObjectStore();
        ChainStateRepositoryImpl repository = repository(cache.asCache(), stateStore);

        ChainState state = repository.create(instanceId);

        Assert.assertEquals(instanceId, state.getInstanceId());
        Assert.assertEquals(1, stateStore.getCreateCount());
        Assert.assertEquals(
                instanceId,
                stateStore.getLastCreatedFields().get(ChainStateField.INSTANCE_ID.name()));
    }

    /**
     * 验证旧 JetCache 状态首次读取后迁移到版本对象存储。
     *
     * @throws Exception 测试依赖注入失败时抛出
     */
    @Test
    public void loadShouldMigrateLegacyStateOnce() throws Exception {
        ChainState legacy = new ChainState();
        legacy.setInstanceId("legacy-instance");
        legacy.setVersion(7L);
        legacy.setStatus(ChainStatus.SUCCEEDED);
        RecordingCache cache = new RecordingCache(
                new CacheGetResult<>(
                        CacheResultCode.SUCCESS,
                        null,
                        new CacheValueHolder<>(legacy, Long.MAX_VALUE)));
        RecordingVersionedObjectStore stateStore = new RecordingVersionedObjectStore();
        ChainStateRepositoryImpl repository = repository(cache.asCache(), stateStore);

        ChainState loaded = repository.load(legacy.getInstanceId());

        Assert.assertNotSame(legacy, loaded);
        Assert.assertEquals(legacy.getInstanceId(), loaded.getInstanceId());
        Assert.assertEquals(legacy.getStatus(), loaded.getStatus());
        Assert.assertEquals(legacy.getVersion(), loaded.getVersion());
        Assert.assertEquals(1, stateStore.getCreateCount());
        Assert.assertEquals(7L, stateStore.getVersionForLastKey());
    }

    /**
     * 验证过期版本不能覆盖已经成功提交的新状态。
     *
     * @throws Exception 测试依赖注入失败时抛出
     */
    @Test
    public void tryUpdateShouldRejectStaleVersion() throws Exception {
        RecordingCache cache = new RecordingCache(
                new CacheGetResult<>(CacheResultCode.NOT_EXISTS, null, null));
        RecordingVersionedObjectStore stateStore = new RecordingVersionedObjectStore();
        ChainStateRepositoryImpl repository = repository(cache.asCache(), stateStore);
        ChainState created = repository.create("cas-instance");

        ChainState firstUpdate = new ChainState();
        firstUpdate.setInstanceId(created.getInstanceId());
        firstUpdate.setVersion(1L);
        Assert.assertTrue(repository.tryUpdate(
                firstUpdate, EnumSet.of(ChainStateField.VERSION)));

        ChainState staleUpdate = new ChainState();
        staleUpdate.setInstanceId(created.getInstanceId());
        staleUpdate.setVersion(1L);
        Assert.assertFalse(repository.tryUpdate(
                staleUpdate, EnumSet.of(ChainStateField.VERSION)));
    }

    /**
     * 验证实例锁成功获取后分配独立的实例 fencing token。
     *
     * @throws Exception 测试依赖注入失败时抛出
     */
    @Test
    public void getLockShouldAllocateInstanceFencingToken() throws Exception {
        RecordingCache cache = new RecordingCache(
                new CacheGetResult<>(CacheResultCode.NOT_EXISTS, null, null));
        ChainStateRepositoryImpl repository = repository(
                cache.asCache(), new RecordingVersionedObjectStore());
        RedisLockExecutor lockExecutor = Mockito.mock(RedisLockExecutor.class);
        RedisLockExecutor.LockHandle handle =
                Mockito.mock(RedisLockExecutor.LockHandle.class);
        Mockito.when(lockExecutor.tryAcquireFenced(
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.any(Duration.class),
                ArgumentMatchers.any(Duration.class),
                ArgumentMatchers.any(Duration.class)
        )).thenReturn(handle);
        Mockito.when(handle.getFencingToken()).thenReturn(17L);
        setField(repository, "redisLockExecutor", lockExecutor);

        ChainLock lock = repository.getLock("fenced-instance", 10L, TimeUnit.SECONDS);
        try {
            Assert.assertTrue(lock.isAcquired());
            Assert.assertEquals(17L, lock.getFencingToken());
        } finally {
            lock.close();
        }

        Mockito.verify(lockExecutor).tryAcquireFenced(
                ArgumentMatchers.eq("chainLock:{fenced-instance}"),
                ArgumentMatchers.eq("workflowState:{fenced-instance}:fence"),
                ArgumentMatchers.any(Duration.class),
                ArgumentMatchers.any(Duration.class),
                ArgumentMatchers.eq(Duration.ofDays(4)));
        Mockito.verify(handle).release();
    }

    /**
     * 验证状态 CAS 同时校验实例锁和 trigger claim 守卫。
     *
     * @throws Exception 测试依赖注入失败时抛出
     */
    @Test
    public void tryUpdateShouldGuardLockAndSpecificTriggerClaim() throws Exception {
        RecordingCache cache = new RecordingCache(
                new CacheGetResult<>(CacheResultCode.NOT_EXISTS, null, null));
        VersionedObjectStore stateStore = Mockito.mock(VersionedObjectStore.class);
        Mockito.when(stateStore.compareAndSetFieldsAndRefresh(
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyLong(),
                ArgumentMatchers.anyMap(),
                ArgumentMatchers.anyLong(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyLong(),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.anyLong(),
                ArgumentMatchers.any(Duration.class),
                ArgumentMatchers.anyString(),
                ArgumentMatchers.any(Duration.class)
        )).thenReturn(true);
        ChainStateRepositoryImpl repository = repository(cache.asCache(), stateStore);
        ChainState update = new ChainState();
        update.setInstanceId("claim-guard-instance");
        update.setVersion(1L);

        Assert.assertTrue(repository.tryUpdate(
                update,
                EnumSet.of(ChainStateField.VERSION),
                17L,
                "trigger-1",
                42L));

        Mockito.verify(stateStore).compareAndSetFieldsAndRefresh(
                ArgumentMatchers.eq("workflowState:{claim-guard-instance}:chain"),
                ArgumentMatchers.eq(0L),
                ArgumentMatchers.anyMap(),
                ArgumentMatchers.eq(1L),
                ArgumentMatchers.eq("workflowState:{claim-guard-instance}:fence"),
                ArgumentMatchers.eq(17L),
                ArgumentMatchers.eq(
                        "workflowState:{claim-guard-instance}:claim:trigger-1"),
                ArgumentMatchers.eq(42L),
                ArgumentMatchers.eq(Duration.ofDays(3)),
                ArgumentMatchers.eq("workflowState:{claim-guard-instance}:format"),
                ArgumentMatchers.eq(Duration.ofDays(4)));
    }

    /**
     * 创建工作流状态仓储并注入测试依赖。
     *
     * @param cache 旧 JetCache 测试代理
     * @param stateStore 版本对象存储
     * @return 已完成依赖注入的仓储
     * @throws Exception 反射注入失败时抛出
     */
    private ChainStateRepositoryImpl repository(Cache<String, Object> cache,
                                                VersionedObjectStore stateStore) throws Exception {
        ChainStateRepositoryImpl repository = new ChainStateRepositoryImpl();
        Field cacheField = BaseRepository.class.getDeclaredField("cache");
        cacheField.setAccessible(true);
        cacheField.set(repository, cache);
        Field storeField = ChainStateRepositoryImpl.class.getDeclaredField("versionedObjectStore");
        storeField.setAccessible(true);
        storeField.set(repository, stateStore);
        return repository;
    }

    /**
     * 反射注入测试依赖。
     *
     * @param target 目标对象
     * @param name 字段名
     * @param value 字段值
     * @throws Exception 字段访问失败时抛出
     */
    private void setField(Object target, String name, Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(target, value);
    }

    /**
     * 仅实现当前仓储测试所需读取操作的 JetCache 代理。
     */
    private static final class RecordingCache implements InvocationHandler {

        private final CacheGetResult<Object> getResult;

        /**
         * 创建 JetCache 调用记录器。
         *
         * @param getResult 读取操作结果
         */
        private RecordingCache(CacheGetResult<Object> getResult) {
            this.getResult = getResult;
        }

        /**
         * 创建实现 JetCache 接口的动态代理。
         *
         * @return JetCache 测试代理
         */
        @SuppressWarnings("unchecked")
        private Cache<String, Object> asCache() {
            return (Cache<String, Object>) Proxy.newProxyInstance(
                    Cache.class.getClassLoader(),
                    new Class<?>[]{Cache.class},
                    this
            );
        }

        /**
         * 处理仓储发起的缓存调用。
         *
         * @param proxy 代理对象
         * @param method 被调用方法
         * @param args 调用参数
         * @return 预设结果
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) {
            if ("GET".equals(method.getName())) {
                return getResult;
            }
            if ("REMOVE".equals(method.getName())) {
                return CacheResult.SUCCESS_WITHOUT_MSG;
            }
            throw new UnsupportedOperationException(
                    "unsupported cache method: " + method.getName());
        }
    }

    /**
     * 以进程内 Map 模拟原子版本对象存储。
     */
    private static final class RecordingVersionedObjectStore implements VersionedObjectStore {

        private final Map<String, Serializable> values = new ConcurrentHashMap<>();
        private final Map<String, Map<String, Object>> fieldValues = new ConcurrentHashMap<>();
        private final Map<String, Long> versions = new ConcurrentHashMap<>();
        private int createCount;
        private Map<String, Object> lastCreatedFields;
        private String lastKey;

        /**
         * {@inheritDoc}
         */
        @Override
        public <T> T load(String key, Class<T> type) {
            Serializable value = values.get(key);
            return value == null ? null : type.cast(value);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public VersionedFields loadFields(String key) {
            Map<String, Object> fields = fieldValues.get(key);
            Long version = versions.get(key);
            return fields == null || version == null
                    ? null
                    : new VersionedFields(version, fields);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized boolean createFieldsIfAbsent(
                String key,
                Map<String, ? extends Serializable> fields,
                long version,
                Duration ttl) {
            if (fieldValues.containsKey(key) || values.containsKey(key)) {
                return false;
            }
            fieldValues.put(key, new LinkedHashMap<>(fields));
            versions.put(key, version);
            if (!key.endsWith(":format")) {
                createCount++;
                lastCreatedFields = new LinkedHashMap<>(fields);
                lastKey = key;
            }
            return true;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized boolean compareAndSetFields(
                String key,
                long expectedVersion,
                Map<String, ? extends Serializable> fields,
                long newVersion,
                Duration ttl) {
            Long currentVersion = versions.get(key);
            if (currentVersion == null || currentVersion != expectedVersion) {
                return false;
            }
            fieldValues.computeIfAbsent(key, ignored -> new LinkedHashMap<>()).putAll(fields);
            versions.put(key, newVersion);
            return true;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized boolean rewriteAsFields(
                String key,
                long expectedVersion,
                Map<String, ? extends Serializable> fields,
                Duration ttl) {
            Long currentVersion = versions.get(key);
            if (currentVersion == null || currentVersion != expectedVersion) {
                return false;
            }
            values.remove(key);
            fieldValues.put(key, new LinkedHashMap<>(fields));
            return true;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized boolean createIfAbsent(String key,
                                                   Serializable value,
                                                   long version,
                                                   Duration ttl) {
            if (values.containsKey(key)) {
                return false;
            }
            values.put(key, value);
            versions.put(key, version);
            createCount++;
            lastKey = key;
            return true;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean createIfAbsent(String key,
                                      Serializable value,
                                      long version,
                                      String guardKey,
                                      long guardVersion,
                                      Duration ttl) {
            Long currentGuard = versions.get(guardKey);
            return currentGuard != null
                    && currentGuard == guardVersion
                    && createIfAbsent(key, value, version, ttl);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public synchronized boolean compareAndSet(String key,
                                                  long expectedVersion,
                                                  Serializable value,
                                                  long newVersion,
                                                  Duration ttl) {
            Long currentVersion = versions.get(key);
            if (currentVersion == null || currentVersion != expectedVersion) {
                return false;
            }
            values.put(key, value);
            versions.put(key, newVersion);
            return true;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean compareAndSet(String key,
                                     long expectedVersion,
                                     Serializable value,
                                     long newVersion,
                                     String guardKey,
                                     long guardVersion,
                                     Duration ttl) {
            Long currentGuard = versions.get(guardKey);
            return currentGuard != null
                    && currentGuard == guardVersion
                    && compareAndSet(key, expectedVersion, value, newVersion, ttl);
        }

        /**
         * 获取创建次数。
         *
         * @return 创建次数
         */
        private int getCreateCount() {
            return createCount;
        }

        /**
         * 获取最后创建的对象。
         *
         * @return 最后创建的对象
         */
        private Map<String, Object> getLastCreatedFields() {
            return lastCreatedFields;
        }

        /**
         * 获取最后写入键的版本。
         *
         * @return 最后写入版本
         */
        private long getVersionForLastKey() {
            return versions.get(lastKey);
        }
    }
}
