package tech.easyflow.ai.easyagentsflow.service;

import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.flow.core.parser.ChainParser;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.node.SearchDatasetNodeParser;
import tech.easyflow.ai.node.WorkflowNodeParser;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * {@link WorkflowRunningParameterResolver} 测试。
 */
public class WorkflowRunningParameterResolverTest {

    /**
     * 应返回显式配置的开场表单元信息与字段 Schema。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testBuildRunningParametersViewShouldKeepStartFormSchema() throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        JSONObject startData = data("开始");
        JSONArray schema = new JSONArray();

        JSONObject userInputField = new JSONObject();
        userInputField.put("key", "user_input");
        userInputField.put("label", "主问题");
        userInputField.put("type", "text");
        userInputField.put("required", true);
        schema.add(userInputField);

        JSONObject fileField = new JSONObject();
        fileField.put("key", "attachments");
        fileField.put("label", "附件");
        fileField.put("type", "file");
        fileField.put("contentType", "file");
        fileField.put("required", false);
        schema.add(fileField);

        JSONObject imageField = new JSONObject();
        imageField.put("key", "preview_image");
        imageField.put("label", "预览图");
        imageField.put("type", "text");
        imageField.put("contentType", "text");
        imageField.put("required", false);
        schema.add(imageField);

        JSONObject meta = new JSONObject();
        meta.put("title", "问答入口");
        meta.put("description", "请先填写信息");
        meta.put("submitText", "立即开始");
        startData.put("startFormMeta", meta);
        startData.put("startFormSchema", schema);
        JSONArray parameters = startParameters();
        JSONObject imageParameter = new JSONObject();
        imageParameter.put("name", "preview_image");
        imageParameter.put("dataType", "Object");
        imageParameter.put("refType", "input");
        imageParameter.put("contentType", "image");
        imageParameter.put("formType", "input");
        imageParameter.put("formLabel", "预览图");
        parameters.add(imageParameter);
        startData.put("parameters", parameters);

        Workflow workflow = workflow(
                workflowJson(
                        array(
                                node("s1", "startNode", null, startData),
                                node("e1", "endNode", null, data("结束"))
                        ),
                        array(edge("e1", "s1", "e1"))
                )
        );

        Map<String, Object> result = resolver.buildRunningParametersView(workflow);
        Assert.assertNotNull(result);
        Assert.assertEquals("问答入口", ((Map<?, ?>) result.get("startFormMeta")).get("title"));
        List<Map<String, Object>> fields = (List<Map<String, Object>>) result.get("startFormSchema");
        Assert.assertEquals(3, fields.size());
        Assert.assertEquals("user_input", fields.get(0).get("key"));
        Assert.assertEquals("text", fields.get(0).get("type"));
        Assert.assertEquals("text", fields.get(0).get("contentType"));
        Assert.assertEquals("attachments", fields.get(1).get("key"));
        Assert.assertEquals("file", fields.get(1).get("type"));
        Assert.assertEquals("file", fields.get(1).get("contentType"));
        Assert.assertEquals("preview_image", fields.get(2).get("key"));
        Assert.assertEquals("text", fields.get(2).get("type"));
        Assert.assertEquals("image", fields.get(2).get("contentType"));
    }

    /**
     * 旧工作流仅保留 parameters 时，也应补出最小开场表单 Schema。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testBuildRunningParametersViewShouldFallbackToParameters() throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        JSONObject startData = data("开始");
        startData.put("parameters", startParameters());

        Workflow workflow = workflow(
                workflowJson(
                        array(
                                node("s1", "startNode", null, startData),
                                node("e1", "endNode", null, data("结束"))
                        ),
                        array(edge("e1", "s1", "e1"))
                )
        );

        Map<String, Object> result = resolver.buildRunningParametersView(workflow);
        Assert.assertNotNull(result);
        List<Map<String, Object>> fields = (List<Map<String, Object>>) result.get("startFormSchema");
        Assert.assertEquals(2, fields.size());
        Assert.assertEquals("user_input", fields.get(0).get("key"));
        Assert.assertEquals("textarea", fields.get(0).get("type"));
        Assert.assertEquals(Boolean.TRUE, fields.get(0).get("required"));
        Assert.assertEquals("attachments", fields.get(1).get("key"));
        Assert.assertEquals("file", fields.get(1).get("type"));
    }

    /**
     * 旧工作流若不存在系统问题参数，运行态不应凭空注入 user_input。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testBuildRunningParametersViewShouldNotInjectUserInputForLegacyParametersWithoutSystemField() throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        JSONObject startData = data("开始");
        startData.put("parameters", startParametersWithoutUserInput());

        Workflow workflow = workflow(
                workflowJson(
                        array(
                                node("s1", "startNode", null, startData),
                                node("e1", "endNode", null, data("结束"))
                        ),
                        array(edge("e1", "s1", "e1"))
                )
        );

        Map<String, Object> result = resolver.buildRunningParametersView(workflow);
        Assert.assertNotNull(result);
        List<Map<String, Object>> fields = (List<Map<String, Object>>) result.get("startFormSchema");
        Assert.assertEquals(1, fields.size());
        Assert.assertEquals("attachments", fields.get(0).get("key"));
        Assert.assertEquals("file", fields.get(0).get("type"));
    }

    /**
     * multipart 文件字段名应从解析后的开始节点参数中按定义顺序返回。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testResolveFileParameterNamesShouldReturnStartFileFields()
            throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();

        Assert.assertEquals(
                List.of("attachments"),
                List.copyOf(resolver.resolveFileParameterNames(
                        workflowContentWithStartParameters())));
    }

    /**
     * 必填文件字段名应从开始节点参数定义中单独解析。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testResolveRequiredFileParameterNamesShouldKeepOrder()
            throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        JSONObject startData = data("开始");
        JSONArray parameters = startParameters();
        parameters.getJSONObject(1).put("required", true);
        startData.put("parameters", parameters);
        String content = workflowJson(
                array(
                        node("s1", "startNode", null, startData),
                        node("e1", "endNode", null, data("结束"))),
                array(edge("e1", "s1", "e1")));

        Assert.assertEquals(
                List.of("attachments"),
                List.copyOf(resolver
                        .resolveRequiredFileParameterNames(content)));
    }

    /**
     * 文件参数运行值应统一归一化为数组并按 filePath 去重。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testNormalizeRuntimeVariablesShouldWrapFileValueIntoArray() throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        Map<String, Object> variables = new LinkedHashMap<>();
        variables.put("attachments", fileValue("demo.pdf", "/files/demo.pdf", 1024L));

        Map<String, Object> normalized = resolver.normalizeRuntimeVariables(workflowContentWithStartParameters(), variables);

        Object attachments = normalized.get("attachments");
        Assert.assertTrue(attachments instanceof List<?>);
        Assert.assertEquals(1, ((List<?>) attachments).size());
        Assert.assertTrue(((List<?>) attachments).get(0) instanceof Map<?, ?>);
    }

    /**
     * 文件参数应接受远程 URL 字符串数组并自动提取文件名。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testNormalizeRuntimeVariablesShouldAcceptRemoteFileUrls()
            throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        String firstUrl = "https://files.example.com/contracts/"
                + "%E5%90%88%E5%90%8C%20v1.docx?signature=test";
        String secondUrl = "https://files.example.com/contracts/report.pdf";
        Map<String, Object> variables = new LinkedHashMap<>();
        variables.put("attachments", List.of(firstUrl, secondUrl));

        Map<String, Object> normalized = resolver.normalizeRuntimeVariables(
                workflowContentWithStartParameters(),
                variables);

        List<?> attachments = (List<?>) normalized.get("attachments");
        Assert.assertEquals(2, attachments.size());
        Assert.assertEquals(
                "合同 v1.docx",
                ((Map<?, ?>) attachments.get(0)).get("fileName"));
        Assert.assertEquals(
                firstUrl,
                ((Map<?, ?>) attachments.get(0)).get("filePath"));
        Assert.assertEquals(
                "report.pdf",
                ((Map<?, ?>) attachments.get(1)).get("fileName"));
    }

    /**
     * 单个远程文件 URL 也应归一化为文件对象数组。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testNormalizeRuntimeVariablesShouldAcceptSingleRemoteFileUrl()
            throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        Map<String, Object> variables = new LinkedHashMap<>();
        variables.put(
                "attachments",
                "https://files.example.com/contracts/contract.docx");

        Map<String, Object> normalized = resolver.normalizeRuntimeVariables(
                workflowContentWithStartParameters(),
                variables);

        List<?> attachments = (List<?>) normalized.get("attachments");
        Assert.assertEquals(1, attachments.size());
        Assert.assertEquals(
                "contract.docx",
                ((Map<?, ?>) attachments.get(0)).get("fileName"));
    }

    /**
     * 无法从 URL 路径识别文件扩展名时应给出可恢复的格式提示。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testNormalizeRuntimeVariablesShouldRejectAmbiguousRemoteUrl()
            throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        Map<String, Object> variables = new LinkedHashMap<>();
        variables.put(
                "attachments",
                "https://files.example.com/download?id=contract");

        BusinessException exception = Assert.assertThrows(
                BusinessException.class,
                () -> resolver.normalizeRuntimeVariables(
                        workflowContentWithStartParameters(),
                        variables));

        Assert.assertEquals(
                "文件参数 attachments 的 URL 路径无法识别带扩展名的文件名，"
                        + "请改用包含 fileName 和 filePath 的文件对象",
                exception.getMessage());
    }

    /**
     * 多文件参数应按 filePath 去重并保留已有非文件变量。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testNormalizeRuntimeVariablesShouldDedupeFileArray() throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        Map<String, Object> variables = new LinkedHashMap<>();
        variables.put("user_input", "hello");
        variables.put("attachments", List.of(
                fileValue("a.pdf", "/files/a.pdf", 1024L),
                fileValue("a-copy.pdf", "/files/a.pdf", 1024L),
                fileValue("b.pdf", "/files/b.pdf", 1024L)
        ));

        Map<String, Object> normalized = resolver.normalizeRuntimeVariables(workflowContentWithStartParameters(), variables);

        Assert.assertEquals("hello", normalized.get("user_input"));
        Object attachments = normalized.get("attachments");
        Assert.assertTrue(attachments instanceof List<?>);
        Assert.assertEquals(2, ((List<?>) attachments).size());
    }

    /**
     * 文件参数应允许 100MB 边界值，并拒绝超过边界的文件。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testNormalizeRuntimeVariablesShouldEnforceHundredMbSingleFileLimit() throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        Map<String, Object> variables = new LinkedHashMap<>();
        variables.put("attachments", fileValue(
                "accepted.pdf",
                "/files/accepted.pdf",
                100L * 1024L * 1024L
        ));

        Map<String, Object> normalized = resolver.normalizeRuntimeVariables(
                workflowContentWithStartParameters(),
                variables
        );
        Assert.assertEquals(1, ((List<?>) normalized.get("attachments")).size());

        variables.put("attachments", fileValue(
                "oversized.pdf",
                "/files/oversized.pdf",
                100L * 1024L * 1024L + 1L
        ));
        try {
            resolver.normalizeRuntimeVariables(workflowContentWithStartParameters(), variables);
            Assert.fail("expected BusinessException");
        } catch (BusinessException exception) {
            Assert.assertEquals("文件参数 attachments 中单个文件不能超过 100MB", exception.getMessage());
        }
    }

    /**
     * 文件参数总大小应允许 100MB 边界值，并拒绝超过边界的文件列表。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testNormalizeRuntimeVariablesShouldEnforceHundredMbTotalFileLimit() throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        Map<String, Object> variables = new LinkedHashMap<>();
        variables.put("attachments", List.of(
                fileValue("first.pdf", "/files/first.pdf", 50L * 1024L * 1024L),
                fileValue("second.pdf", "/files/second.pdf", 50L * 1024L * 1024L)
        ));

        Map<String, Object> normalized = resolver.normalizeRuntimeVariables(
                workflowContentWithStartParameters(),
                variables
        );
        Assert.assertEquals(2, ((List<?>) normalized.get("attachments")).size());

        variables.put("attachments", List.of(
                fileValue("first.pdf", "/files/first.pdf", 50L * 1024L * 1024L),
                fileValue("second.pdf", "/files/second.pdf", 50L * 1024L * 1024L + 1L)
        ));
        try {
            resolver.normalizeRuntimeVariables(workflowContentWithStartParameters(), variables);
            Assert.fail("expected BusinessException");
        } catch (BusinessException exception) {
            Assert.assertEquals(
                    "文件参数 attachments 的文件总大小不能超过 100MB",
                    exception.getMessage());
        }
    }

    /**
     * 文件参数应拒绝超过十个文件的输入。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testNormalizeRuntimeVariablesShouldEnforceFileCountLimit()
            throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        List<Map<String, Object>> files = new java.util.ArrayList<>();
        for (int index = 0; index < 11; index++) {
            files.add(fileValue(
                    "file-" + index + ".pdf",
                    "/files/file-" + index + ".pdf",
                    1L));
        }
        Map<String, Object> variables = new LinkedHashMap<>();
        variables.put("attachments", files);

        BusinessException exception = Assert.assertThrows(
                BusinessException.class,
                () -> resolver.normalizeRuntimeVariables(
                        workflowContentWithStartParameters(),
                        variables));

        Assert.assertEquals(
                "文件参数 attachments 最多上传 10 个文件",
                exception.getMessage());
    }

    /**
     * 旧版图片 URL 应归一化为 URL 图片描述。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testNormalizeRuntimeVariablesShouldNormalizeLegacyImageUrl() throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        Map<String, Object> variables = new LinkedHashMap<>();
        variables.put("image_input", "https://example.com/image.png");

        Map<String, Object> normalized = resolver.normalizeRuntimeVariables(
                workflowContentWithImageStartParameter(),
                variables
        );

        Assert.assertTrue(normalized.get("image_input") instanceof Map<?, ?>);
        Map<?, ?> image = (Map<?, ?>) normalized.get("image_input");
        Assert.assertEquals("url", image.get("sourceType"));
        Assert.assertEquals("https://example.com/image.png", image.get("url"));
    }

    /**
     * 运行入口不应接收 Data URI，避免 Base64 写入工作流状态和审计参数。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testNormalizeRuntimeVariablesShouldRejectImageDataUri() throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        Map<String, Object> variables = new LinkedHashMap<>();
        variables.put("image_input", "data:image/png;base64,AQID");

        try {
            resolver.normalizeRuntimeVariables(workflowContentWithImageStartParameter(), variables);
            Assert.fail("expected BusinessException");
        } catch (BusinessException exception) {
            Assert.assertEquals(
                    "图片参数 image_input 仅支持 HTTP/HTTPS 图片 URL",
                    exception.getMessage());
        }
    }

    /**
     * 图片参数应允许 10 MiB 边界并拒绝更大的声明值。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void testNormalizeRuntimeVariablesShouldEnforceImageLimit() throws Exception {
        WorkflowRunningParameterResolver resolver = newResolver();
        Map<String, Object> variables = new LinkedHashMap<>();
        variables.put("image_input", imageValue(10L * 1024L * 1024L));

        Map<String, Object> normalized = resolver.normalizeRuntimeVariables(
                workflowContentWithImageStartParameter(),
                variables
        );
        Assert.assertEquals("upload", ((Map<?, ?>) normalized.get("image_input")).get("sourceType"));

        variables.put("image_input", imageValue(10L * 1024L * 1024L + 1L));
        try {
            resolver.normalizeRuntimeVariables(workflowContentWithImageStartParameter(), variables);
            Assert.fail("expected BusinessException");
        } catch (BusinessException exception) {
            Assert.assertEquals(
                    "图片参数 image_input 中图片不能超过 10 MiB",
                    exception.getMessage());
        }
    }

    private static WorkflowRunningParameterResolver newResolver() throws Exception {
        WorkflowRunningParameterResolver resolver = new WorkflowRunningParameterResolver();
        ChainParser parser = ChainParser.builder()
                .withDefaultParsers(true)
                .build();
        parser.addNodeParser("workflow-node", new WorkflowNodeParser());
        parser.addNodeParser("search-dataset-node", new SearchDatasetNodeParser());
        setField(resolver, "chainParser", parser);
        setField(resolver, "workflowDatacenterContentService", new WorkflowDatacenterContentService());
        return resolver;
    }

    private static Workflow workflow(String content) {
        Workflow workflow = new Workflow();
        workflow.setId(BigInteger.ONE);
        workflow.setTitle("测试工作流");
        workflow.setDescription("用于测试运行参数");
        workflow.setIcon("icon.png");
        workflow.setContent(content);
        return workflow;
    }

    private static String workflowContentWithStartParameters() {
        JSONObject startData = data("开始");
        startData.put("parameters", startParameters());
        return workflowJson(
                array(
                        node("s1", "startNode", null, startData),
                        node("e1", "endNode", null, data("结束"))
                ),
                array(edge("e1", "s1", "e1"))
        );
    }

    private static String workflowContentWithImageStartParameter() {
        JSONObject startData = data("开始");
        JSONArray parameters = new JSONArray();
        JSONObject imageField = new JSONObject();
        imageField.put("name", "image_input");
        imageField.put("dataType", "Object");
        imageField.put("refType", "input");
        imageField.put("contentType", "image");
        imageField.put("formType", "input");
        parameters.add(imageField);
        startData.put("parameters", parameters);
        return workflowJson(
                array(
                        node("s1", "startNode", null, startData),
                        node("e1", "endNode", null, data("结束"))
                ),
                array(edge("e1", "s1", "e1"))
        );
    }

    private static JSONArray startParameters() {
        JSONArray parameters = new JSONArray();

        JSONObject userInput = new JSONObject();
        userInput.put("name", "user_input");
        userInput.put("dataType", "String");
        userInput.put("refType", "input");
        userInput.put("required", true);
        userInput.put("contentType", "text");
        userInput.put("formType", "textarea");
        userInput.put("formLabel", "用户问题");
        userInput.put("formPlaceholder", "请输入用户问题");
        parameters.add(userInput);

        JSONObject fileField = new JSONObject();
        fileField.put("name", "attachments");
        fileField.put("dataType", "File");
        fileField.put("refType", "input");
        fileField.put("required", false);
        fileField.put("contentType", "file");
        fileField.put("formType", "input");
        fileField.put("formLabel", "附件");
        parameters.add(fileField);
        return parameters;
    }

    private static JSONArray startParametersWithoutUserInput() {
        JSONArray parameters = new JSONArray();

        JSONObject fileField = new JSONObject();
        fileField.put("name", "attachments");
        fileField.put("dataType", "File");
        fileField.put("refType", "input");
        fileField.put("required", false);
        fileField.put("contentType", "file");
        fileField.put("formType", "input");
        fileField.put("formLabel", "附件");
        parameters.add(fileField);
        return parameters;
    }

    private static Map<String, Object> fileValue(String fileName, String filePath, Long size) {
        Map<String, Object> value = new LinkedHashMap<>();
        value.put("fileName", fileName);
        value.put("filePath", filePath);
        value.put("size", size);
        value.put("contentType", "application/pdf");
        value.put("url", filePath);
        return value;
    }

    private static Map<String, Object> imageValue(long size) {
        Map<String, Object> value = new LinkedHashMap<>();
        value.put("sourceType", "upload");
        value.put("fileName", "image.png");
        value.put("filePath", "/files/image.png");
        value.put("size", size);
        value.put("contentType", "image/png");
        return value;
    }

    private static void setField(Object target, String fieldName, Object value) throws Exception {
        Field field = WorkflowRunningParameterResolver.class.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(target, value);
    }

    private static String workflowJson(JSONArray nodes, JSONArray edges) {
        JSONObject root = new JSONObject();
        root.put("nodes", nodes);
        root.put("edges", edges);
        return root.toJSONString();
    }

    private static JSONArray array(JSONObject... objects) {
        JSONArray array = new JSONArray();
        for (JSONObject object : objects) {
            array.add(object);
        }
        return array;
    }

    private static JSONObject node(String id, String type, String parentId, JSONObject data) {
        JSONObject node = new JSONObject();
        node.put("id", id);
        node.put("type", type);
        if (parentId != null) {
            node.put("parentId", parentId);
        }
        node.put("data", data);
        return node;
    }

    private static JSONObject data(String title) {
        JSONObject data = new JSONObject();
        data.put("title", title);
        return data;
    }

    private static JSONObject edge(String id, String source, String target) {
        JSONObject edge = new JSONObject();
        edge.put("id", id);
        edge.put("source", source);
        edge.put("target", target);
        return edge;
    }
}
