package com.easyagents.flow.core.test;

import com.alibaba.fastjson.JSONObject;
import com.easyagents.flow.core.chain.Chain;
import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.ChainState;
import com.easyagents.flow.core.chain.Parameter;
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository;
import com.easyagents.flow.core.code.impl.JavascriptExecutionException;
import com.easyagents.flow.core.code.impl.JavascriptRuntimeEngine;
import com.easyagents.flow.core.node.CodeNode;
import com.easyagents.flow.core.parser.impl.CodeNodeParser;
import org.junit.Assert;
import org.junit.Test;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * JavaScript 代码节点输出约定回归测试。
 */
public class JavascriptRuntimeEngineTest {

    /**
     * 验证运行时自动调用 main，并把节点输入作为对象参数传入。
     */
    @Test
    public void shouldInvokeMainAndUseReturnedObject() {
        CodeNode node = codeNode(
                String.join("\n",
                        "function main({ data1, data2 }) {",
                        "  return {",
                        "    joined: data1 + data2,",
                        "    totalLength: data1.length + data2.length",
                        "  };",
                        "}"),
                "data1",
                "data2");
        Chain chain = chain(Map.of(
                "data1", "河北",
                "data2", "分行"));

        Map<String, Object> result = node.execute(chain);

        Assert.assertEquals("河北分行", result.get("joined"));
        Assert.assertEquals(4L, result.get("totalLength"));
    }

    /**
     * 验证新节点按配置顺序把输入值传给 main 的独立形参。
     */
    @Test
    public void shouldInvokeMainWithNamedArguments() {
        CodeNode node = codeNode(
                String.join("\n",
                        "function main(data1, data2) {",
                        "  return { joined: data1 + data2 };",
                        "}"),
                "data1",
                "data2");
        node.setMainArgsMode(CodeNode.MAIN_ARGS_MODE_NAMED);
        Chain chain = chain(Map.of(
                "data1", "河北",
                "data2", "分行"));

        Map<String, Object> result = node.execute(chain);

        Assert.assertEquals("河北分行", result.get("joined"));
    }

    /**
     * 验证解析器保留设计器声明的 main 参数模式。
     */
    @Test
    public void shouldParseMainArgsMode() {
        JSONObject data = new JSONObject();
        data.put("engine", "js");
        data.put("code", "function main(data) { return { data }; }");
        data.put("mainArgsMode", CodeNode.MAIN_ARGS_MODE_NAMED);

        CodeNode node = new CodeNodeParser().doParse(
                new JSONObject(), data, new JSONObject());

        Assert.assertEquals(
                CodeNode.MAIN_ARGS_MODE_NAMED,
                node.getMainArgsMode());
    }

    /**
     * 验证已有 _result 输出优先，避免自动调用 main 改变历史流程。
     */
    @Test
    public void shouldKeepLegacyResultWithoutInvokingMain() {
        CodeNode node = codeNode(
                String.join("\n",
                        "_result.answer = 'legacy';",
                        "function main() {",
                        "  throw new Error('main should not be invoked');",
                        "}"));
        Chain chain = chain(Collections.emptyMap());

        Map<String, Object> result = node.execute(chain);

        Assert.assertEquals("legacy", result.get("answer"));
    }

    /**
     * 验证显式调用返回空对象时不会再次自动调用 main。
     */
    @Test
    public void shouldKeepEmptyExplicitResultWithoutInvokingMainAgain() {
        CodeNode node = codeNode(
                String.join("\n",
                        "function main(data) {",
                        "  if (typeof data !== 'string') {",
                        "    throw new Error('main invoked twice');",
                        "  }",
                        "  return {};",
                        "}",
                        "_result = main(data);"),
                "data");
        Chain chain = chain(Map.of("data", "hello"));

        Map<String, Object> result = node.execute(chain);

        Assert.assertTrue(result.isEmpty());
    }

    /**
     * 验证 main 返回非对象时给出明确错误。
     */
    @Test
    public void shouldRejectNonObjectMainResult() {
        CodeNode node = codeNode(
                "function main() { return 'invalid'; }");
        Chain chain = chain(Collections.emptyMap());

        try {
            node.execute(chain);
            Assert.fail("main 返回非对象时应执行失败");
        } catch (RuntimeException exception) {
            Assert.assertTrue(exception.getMessage().contains(
                    "JavaScript main 函数必须返回对象"));
        }
    }

    /**
     * 验证语法错误包含可定位的脚本行列信息。
     */
    @Test
    public void shouldExposeSyntaxErrorLocation() {
        CodeNode node = codeNode("const answer = ;");
        Chain chain = chain(Collections.emptyMap());

        try {
            node.execute(chain);
            Assert.fail("JavaScript 语法错误应执行失败");
        } catch (JavascriptExecutionException exception) {
            Assert.assertTrue(exception.getMessage().contains(
                    "JavaScript 语法错误"));
            Assert.assertTrue(exception.getMessage().contains("第 1 行"));
        }
    }

    /**
     * 验证运行时异常包含原始错误和脚本位置。
     */
    @Test
    public void shouldExposeRuntimeErrorLocation() {
        CodeNode node = codeNode("throw new Error('boom');");
        Chain chain = chain(Collections.emptyMap());

        try {
            node.execute(chain);
            Assert.fail("JavaScript 运行时异常应执行失败");
        } catch (JavascriptExecutionException exception) {
            Assert.assertTrue(exception.getMessage().contains(
                    "JavaScript 执行失败"));
            Assert.assertTrue(exception.getMessage().contains("第 1 行"));
            Assert.assertTrue(exception.getMessage().contains("boom"));
        }
    }

    /**
     * 验证无限循环会在配置时间内被取消。
     */
    @Test
    public void shouldCancelExecutionAfterTimeout() {
        JavascriptRuntimeEngine engine =
                new JavascriptRuntimeEngine(100L);
        CodeNode node = codeNode("while (true) {}");
        Chain chain = chain(Collections.emptyMap());
        long startedAt = System.currentTimeMillis();

        try {
            engine.execute(
                    node.getCode(),
                    node,
                    chain,
                    Collections.emptyMap());
            Assert.fail("JavaScript 超时应执行失败");
        } catch (JavascriptExecutionException exception) {
            Assert.assertTrue(exception.getMessage().contains(
                    "JavaScript 执行超时（100ms）"));
            Assert.assertTrue(
                    "超时取消应在合理时间内完成",
                    System.currentTimeMillis() - startedAt < 3000L);
        }
    }

    /**
     * 验证负数超时不会静默关闭执行保护。
     */
    @Test
    public void shouldRejectNegativeTimeout() {
        try {
            new JavascriptRuntimeEngine(-1L);
            Assert.fail("负数超时应被拒绝");
        } catch (IllegalArgumentException exception) {
            Assert.assertTrue(exception.getMessage().contains(
                    "不能为负数"));
        }
    }

    /**
     * 验证多个无限循环可并发触发超时取消。
     *
     * @throws Exception 并发任务执行失败时抛出
     */
    @Test
    public void shouldCancelConcurrentExecutionsAfterTimeout()
            throws Exception {
        int executionCount = 8;
        JavascriptRuntimeEngine engine =
                new JavascriptRuntimeEngine(100L);
        ExecutorService executor =
                Executors.newFixedThreadPool(executionCount);
        CountDownLatch ready =
                new CountDownLatch(executionCount);
        CountDownLatch start = new CountDownLatch(1);
        List<Future<String>> futures =
                new ArrayList<>(executionCount);
        try {
            for (int index = 0;
                 index < executionCount;
                 index++) {
                final int executionIndex = index;
                futures.add(executor.submit(() -> {
                    CodeNode node = codeNode("while (true) {}");
                    node.setId("timeout-node-" + executionIndex);
                    Chain chain = chain(Collections.emptyMap());
                    ready.countDown();
                    start.await();
                    try {
                        engine.execute(
                                node.getCode(),
                                node,
                                chain,
                                Collections.emptyMap());
                        return "未触发超时";
                    } catch (JavascriptExecutionException exception) {
                        return exception.getMessage();
                    }
                }));
            }
            Assert.assertTrue(
                    "并发执行未及时就绪",
                    ready.await(2L, TimeUnit.SECONDS));
            long startedAt = System.currentTimeMillis();
            start.countDown();
            for (Future<String> future : futures) {
                Assert.assertTrue(
                        future.get(3L, TimeUnit.SECONDS)
                                .contains("JavaScript 执行超时（100ms）"));
            }
            Assert.assertTrue(
                    "并发超时取消应在合理时间内完成",
                    System.currentTimeMillis() - startedAt < 3000L);
        } finally {
            start.countDown();
            executor.shutdownNow();
        }
    }

    /**
     * 验证分段 Source 缓存在并发未命中时保持结果正确。
     *
     * @throws Exception 并发任务执行失败时抛出
     */
    @Test
    public void shouldExecuteCacheMissesConcurrently()
            throws Exception {
        int executionCount = 32;
        JavascriptRuntimeEngine engine =
                new JavascriptRuntimeEngine();
        ExecutorService executor =
                Executors.newFixedThreadPool(8);
        List<Future<Long>> futures =
                new ArrayList<>(executionCount);
        try {
            for (int index = 0;
                 index < executionCount;
                 index++) {
                final int expected = index;
                futures.add(executor.submit(() -> {
                    CodeNode node = codeNode(
                            "_result.value = " + expected + ";");
                    node.setId("cache-node-" + expected);
                    Map<String, Object> result = engine.execute(
                            node.getCode(),
                            node,
                            chain(Collections.emptyMap()),
                            Collections.emptyMap());
                    return ((Number) result.get("value"))
                            .longValue();
                }));
            }
            for (int index = 0;
                 index < executionCount;
                 index++) {
                Assert.assertEquals(
                        index,
                        futures.get(index)
                                .get(3L, TimeUnit.SECONDS)
                                .longValue());
            }
        } finally {
            executor.shutdownNow();
        }
    }

    /**
     * 验证分段缓存总条目数始终受上限约束。
     *
     * @throws Exception 反射访问缓存失败时抛出
     */
    @Test
    @SuppressWarnings("unchecked")
    public void shouldKeepSourceCacheBounded()
            throws Exception {
        JavascriptRuntimeEngine engine =
                new JavascriptRuntimeEngine();
        Method sourceMethod = JavascriptRuntimeEngine.class
                .getDeclaredMethod(
                        "source",
                        String.class,
                        CodeNode.class);
        sourceMethod.setAccessible(true);
        CodeNode node = codeNode("");
        for (int index = 0; index < 1024; index++) {
            String code = "_result.value = " + index + ";";
            node.setCode(code);
            sourceMethod.invoke(engine, code, node);
        }

        Field cacheField = JavascriptRuntimeEngine.class
                .getDeclaredField("SOURCE_CACHE_SEGMENTS");
        cacheField.setAccessible(true);
        List<Map<String, Object>> segments =
                (List<Map<String, Object>>) cacheField.get(null);
        int cachedSourceCount = segments.stream()
                .mapToInt(Map::size)
                .sum();

        Assert.assertTrue(cachedSourceCount <= 512);
        Assert.assertTrue(
                segments.stream()
                        .allMatch(segment ->
                                segment.size() <= 32));
    }

    /**
     * 验证共享 Engine 下不同执行仍保持全局变量隔离。
     */
    @Test
    public void shouldKeepExecutionContextsIsolated() {
        CodeNode node = codeNode(String.join("\n",
                "if (typeof globalCounter === 'undefined') {",
                "  globalCounter = 0;",
                "}",
                "globalCounter += 1;",
                "_result.counter = globalCounter;"));

        Map<String, Object> first =
                node.execute(chain(Collections.emptyMap()));
        Map<String, Object> second =
                node.execute(chain(Collections.emptyMap()));

        Assert.assertEquals(1L, first.get("counter"));
        Assert.assertEquals(1L, second.get("counter"));
    }

    /**
     * 创建 JavaScript 代码节点。
     *
     * @param code 用户代码
     * @param parameterNames 输入参数名
     * @return 代码节点
     */
    private CodeNode codeNode(String code, String... parameterNames) {
        CodeNode node = new CodeNode();
        node.setId("code-node");
        node.setName("代码节点");
        node.setEngine("js");
        node.setCode(code);
        node.setParameters(Arrays.stream(parameterNames)
                .map(Parameter::new)
                .collect(Collectors.toList()));
        return node;
    }

    /**
     * 创建带初始化状态和输入变量的工作流。
     *
     * @param inputs 工作流输入
     * @return 工作流
     */
    private Chain chain(Map<String, Object> inputs) {
        Chain chain = new Chain(
                new ChainDefinition(),
                "javascript-runtime-" + UUID.randomUUID());
        chain.setChainStateRepository(
                new InMemoryChainStateRepository());
        chain.setNodeStateRepository(
                new InMemoryNodeStateRepository());
        ChainState state = chain.initializeState();
        state.getMemory().putAll(inputs);
        return chain;
    }
}
