package tech.easyflow.publicapi.controller;

import com.easyagents.flow.core.chain.runtime.ChainExecutor;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import tech.easyflow.ai.easyagentsflow.service.WorkflowCheckService;
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
import tech.easyflow.ai.easyagentsflow.upload.WorkflowApiPreparedUpload;
import tech.easyflow.ai.easyagentsflow.upload.WorkflowApiUploadLifecycleService;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.ai.service.WorkflowApiPermissionService;
import tech.easyflow.ai.service.WorkflowService;
import tech.easyflow.common.web.error.GlobalErrorResolver;
import tech.easyflow.publicapi.dto.PublicWorkflowTopology;
import tech.easyflow.publicapi.interceptor.PublicApiRequestContextFilter;
import tech.easyflow.publicapi.service.PublicWorkflowTopologyService;
import tech.easyflow.publicapi.service.WorkflowApiMultipartParameterMapper;
import tech.easyflow.system.entity.SysApiKey;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * {@link PublicWorkflowController} 真实 MVC 路由与错误契约测试。
 */
public class PublicWorkflowControllerRoutingTest {

    private static final String RUN_PATH =
            "/public-api/workflow/runAsync";

    private MockMvc mockMvc;
    private WorkflowService workflowService;
    private WorkflowRunningParameterResolver parameterResolver;
    private WorkflowApiMultipartParameterMapper multipartMapper;
    private WorkflowApiUploadLifecycleService uploadLifecycleService;

    /**
     * 创建控制器和全部最小依赖。
     */
    @Before
    public void setUp() {
        PublicWorkflowController controller =
                new PublicWorkflowController();
        workflowService = Mockito.mock(WorkflowService.class);
        WorkflowCheckService workflowCheckService =
                Mockito.mock(WorkflowCheckService.class);
        WorkflowApiPermissionService permissionService =
                Mockito.mock(WorkflowApiPermissionService.class);
        PublicWorkflowTopologyService topologyService =
                Mockito.mock(PublicWorkflowTopologyService.class);
        ChainExecutor chainExecutor =
                Mockito.mock(ChainExecutor.class);
        parameterResolver = Mockito.mock(
                WorkflowRunningParameterResolver.class);
        multipartMapper = Mockito.mock(
                WorkflowApiMultipartParameterMapper.class);
        uploadLifecycleService = Mockito.mock(
                WorkflowApiUploadLifecycleService.class);

        Workflow workflow = publishedWorkflow();
        SysApiKey apiKey = new SysApiKey();
        apiKey.setId(BigInteger.TEN);
        when(permissionService.assertWorkflowApi(any(), anyString()))
                .thenReturn(apiKey);
        when(workflowService.getPublishedById(BigInteger.ONE))
                .thenReturn(workflow);
        when(workflowService.getPublishedDetail("document-parser"))
                .thenReturn(workflow);
        when(topologyService.resolve(workflow))
                .thenReturn(topology());
        when(parameterResolver.normalizeRuntimeVariables(
                        eq("{}"),
                        anyMap()))
                .thenAnswer(invocation -> invocation.getArgument(1));
        when(chainExecutor.executeAsync(
                        anyString(),
                        anyMap(),
                        Mockito.<Consumer<String>>any()))
                .thenAnswer(invocation -> {
                    Consumer<String> beforeStart =
                            invocation.getArgument(2);
                    if (beforeStart != null) {
                        beforeStart.accept("execute-1");
                    }
                    return "execute-1";
                });
        when(multipartMapper.map(any()))
                .thenReturn(Map.of("file", List.of()));
        when(uploadLifecycleService.prepare(
                        anyString(),
                        eq("{}"),
                        anyMap(),
                        anyMap()))
                .thenReturn(new WorkflowApiPreparedUpload(
                        "upload-1",
                        Map.of()));

        ReflectionTestUtils.setField(
                controller,
                "workflowService",
                workflowService);
        ReflectionTestUtils.setField(
                controller,
                "workflowCheckService",
                workflowCheckService);
        ReflectionTestUtils.setField(
                controller,
                "workflowApiPermissionService",
                permissionService);
        ReflectionTestUtils.setField(
                controller,
                "publicWorkflowTopologyService",
                topologyService);
        ReflectionTestUtils.setField(
                controller,
                "chainExecutor",
                chainExecutor);
        ReflectionTestUtils.setField(
                controller,
                "workflowRunningParameterResolver",
                parameterResolver);
        ReflectionTestUtils.setField(
                controller,
                "workflowApiMultipartParameterMapper",
                multipartMapper);
        ReflectionTestUtils.setField(
                controller,
                "workflowApiUploadLifecycleService",
                uploadLifecycleService);

        LocalValidatorFactoryBean validator =
                new LocalValidatorFactoryBean();
        validator.afterPropertiesSet();
        mockMvc = MockMvcBuilders.standaloneSetup(controller)
                .setValidator(validator)
                .setHandlerExceptionResolvers(
                        new GlobalErrorResolver())
                .addFilters(new PublicApiRequestContextFilter())
                .build();
    }

    /**
     * 验证 application/json 只进入 JSON 调用链。
     *
     * @throws Exception 请求执行失败
     */
    @Test
    public void jsonRequestShouldUseJsonHandler() throws Exception {
        mockMvc.perform(post(RUN_PATH)
                        .header("ApiKey", "key")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"id\":1,\"variables\":{}}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").value("execute-1"));

        verify(parameterResolver).normalizeRuntimeVariables(
                eq("{}"),
                anyMap());
        verify(uploadLifecycleService, never()).prepare(
                anyString(),
                anyString(),
                anyMap(),
                anyMap());
    }

    /**
     * 验证 multipart/form-data 只进入文件直传调用链。
     *
     * @throws Exception 请求执行失败
     */
    @Test
    public void multipartRequestShouldUseMultipartHandler()
            throws Exception {
        MockMultipartFile metadata = new MockMultipartFile(
                "metadata",
                "metadata.json",
                MediaType.APPLICATION_JSON_VALUE,
                "{\"id\":1,\"variables\":{}}"
                        .getBytes(StandardCharsets.UTF_8));
        MockMultipartFile file = new MockMultipartFile(
                "files.file",
                "report.pdf",
                MediaType.APPLICATION_PDF_VALUE,
                "pdf".getBytes(StandardCharsets.UTF_8));

        mockMvc.perform(multipart(RUN_PATH)
                        .file(metadata)
                        .file(file)
                        .header("ApiKey", "key")
                        .header("X-Request-Id", "request-multipart"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data").value("execute-1"));

        verify(multipartMapper).map(any());
        verify(uploadLifecycleService).prepare(
                eq("request-multipart"),
                eq("{}"),
                anyMap(),
                anyMap());
    }

    /**
     * 验证错误顶层媒体类型返回 41501。
     *
     * @throws Exception 请求执行失败
     */
    @Test
    public void unsupportedContentTypeShouldExplainExpectedTypes()
            throws Exception {
        mockMvc.perform(post(RUN_PATH)
                        .header("ApiKey", "key")
                        .header("X-Request-Id", "request-415")
                        .contentType(MediaType.TEXT_PLAIN)
                        .content("{}"))
                .andExpect(status().isUnsupportedMediaType())
                .andExpect(jsonPath("$.errorCode").value(41501))
                .andExpect(jsonPath("$.data.field")
                        .value("Content-Type"))
                .andExpect(jsonPath("$.data.requestId")
                        .value("request-415"))
                .andExpect(header().string(
                        "X-Request-Id",
                        "request-415"));
    }

    /**
     * 验证缺少顶层媒体类型时返回 41501。
     *
     * @throws Exception 请求执行失败
     */
    @Test
    public void missingContentTypeShouldExplainExpectedTypes()
            throws Exception {
        mockMvc.perform(post(RUN_PATH)
                        .header("ApiKey", "key")
                        .content("{}"))
                .andExpect(status().isUnsupportedMediaType())
                .andExpect(jsonPath("$.errorCode").value(41501))
                .andExpect(jsonPath("$.data.expected.length()")
                        .value(2));
    }

    /**
     * 验证无效 JSON 返回 40011，不再误报 ID 为空。
     *
     * @throws Exception 请求执行失败
     */
    @Test
    public void malformedJsonShouldReturnBodyMismatchError()
            throws Exception {
        mockMvc.perform(post(RUN_PATH)
                        .header("ApiKey", "key")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("not-json"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errorCode").value(40011));
    }

    /**
     * 验证 Multipart 缺少 metadata 时返回 40013。
     *
     * @throws Exception 请求执行失败
     */
    @Test
    public void missingMetadataShouldReturnSpecificError()
            throws Exception {
        MockMultipartFile file = new MockMultipartFile(
                "files.file",
                "report.pdf",
                MediaType.APPLICATION_PDF_VALUE,
                "pdf".getBytes(StandardCharsets.UTF_8));

        mockMvc.perform(multipart(RUN_PATH)
                        .file(file)
                        .header("ApiKey", "key"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errorCode").value(40013));
    }

    /**
     * 验证 metadata Part 媒体类型错误时返回 41502。
     *
     * @throws Exception 请求执行失败
     */
    @Test
    public void metadataWithWrongContentTypeShouldReturnSpecificError()
            throws Exception {
        MockMultipartFile metadata = new MockMultipartFile(
                "metadata",
                "metadata.txt",
                MediaType.TEXT_PLAIN_VALUE,
                "{\"id\":1,\"variables\":{}}"
                        .getBytes(StandardCharsets.UTF_8));

        mockMvc.perform(multipart(RUN_PATH)
                        .file(metadata)
                        .header("ApiKey", "key"))
                .andExpect(status().isUnsupportedMediaType())
                .andExpect(jsonPath("$.errorCode").value(41502))
                .andExpect(jsonPath("$.data.expected[0]")
                        .value(MediaType.APPLICATION_JSON_VALUE));
    }

    /**
     * 验证 Multipart metadata 缺少工作流 ID 时返回 40015。
     *
     * @throws Exception 请求执行失败
     */
    @Test
    public void metadataWithoutIdShouldReturnSpecificError()
            throws Exception {
        MockMultipartFile metadata = new MockMultipartFile(
                "metadata",
                "metadata.json",
                MediaType.APPLICATION_JSON_VALUE,
                "{\"variables\":{}}"
                        .getBytes(StandardCharsets.UTF_8));

        mockMvc.perform(multipart(RUN_PATH)
                        .file(metadata)
                        .header("ApiKey", "key"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errorCode").value(40015))
                .andExpect(jsonPath("$.data.field").value("id"));
    }

    /**
     * 验证 metadata Part 内容不是合法 JSON 时返回 40014。
     *
     * @throws Exception 请求执行失败
     */
    @Test
    public void malformedMetadataShouldReturnSpecificError()
            throws Exception {
        MockMultipartFile metadata = new MockMultipartFile(
                "metadata",
                "metadata.json",
                MediaType.APPLICATION_JSON_VALUE,
                "not-json".getBytes(StandardCharsets.UTF_8));

        mockMvc.perform(multipart(RUN_PATH)
                        .file(metadata)
                        .header("ApiKey", "key"))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.errorCode").value(40014));
    }

    /**
     * 验证详情接口只返回安全基础字段。
     *
     * @throws Exception 请求执行失败
     */
    @Test
    public void workflowDetailShouldReturnSafeInfo()
            throws Exception {
        mockMvc.perform(get(
                        "/public-api/workflow/getByIdOrAlias")
                        .header("ApiKey", "key")
                        .param("key", "document-parser"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.id").value("1"))
                .andExpect(jsonPath("$.data.content").doesNotExist())
                .andExpect(jsonPath("$.data.tenantId").doesNotExist())
                .andExpect(jsonPath("$.data.deptId").doesNotExist())
                .andExpect(jsonPath("$.data.publishedSnapshotJson")
                        .doesNotExist());
    }

    /**
     * 验证运行参数解析失败时返回真实 HTTP 500 和稳定错误码。
     *
     * @throws Exception 请求执行失败
     */
    @Test
    public void invalidRunningParametersShouldReturnServerError()
            throws Exception {
        when(parameterResolver.buildRunningParametersView(any()))
                .thenReturn(null);

        mockMvc.perform(get(
                        "/public-api/workflow/getRunningParameters")
                        .header("ApiKey", "key")
                        .param("id", "1"))
                .andExpect(status().isInternalServerError())
                .andExpect(jsonPath("$.errorCode").value(50001));
    }

    /**
     * 创建已发布工作流桩。
     *
     * @return 已发布工作流
     */
    private Workflow publishedWorkflow() {
        Workflow workflow = new Workflow();
        workflow.setId(BigInteger.ONE);
        workflow.setAlias("document-parser");
        workflow.setTitle("文档解析");
        workflow.setContent("{}");
        workflow.setPublishStatus(PublishStatus.PUBLISHED.getCode());
        workflow.setPublishedSnapshotJson(Map.of("nodes", List.of()));
        return workflow;
    }

    /**
     * 创建最小公开拓扑。
     *
     * @return 公开拓扑
     */
    private PublicWorkflowTopology topology() {
        return new PublicWorkflowTopology(
                "1",
                null,
                "测试工作流",
                null,
                1,
                null,
                List.of(),
                List.of(),
                List.of(),
                List.of(),
                false,
                List.of());
    }
}
