package tech.easyflow.publicapi.error;

import com.alibaba.fastjson.JSONObject;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.ModelAndView;
import tech.easyflow.common.web.error.GlobalErrorResolver;
import tech.easyflow.common.web.error.RequestErrorProfile;
import tech.easyflow.common.web.error.RequestIdContext;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.util.List;

/**
 * {@link WorkflowRunAsyncErrorProfile} 错误契约测试。
 */
public class WorkflowRunAsyncErrorProfileTest {

    private final GlobalErrorResolver resolver =
            new GlobalErrorResolver();

    /**
     * 验证缺少 Multipart boundary 时返回可执行修复信息。
     */
    @Test
    public void shouldExplainMissingMultipartBoundary() {
        Resolution resolution = resolve(
                MediaType.MULTIPART_FORM_DATA_VALUE,
                new MultipartException("failed to parse multipart"));

        Assert.assertEquals(400, resolution.response.getStatus());
        Assert.assertEquals(
                40012,
                resolution.modelAndView.getModel().get("errorCode"));
        Assert.assertTrue(String.valueOf(
                        resolution.modelAndView.getModel().get("message"))
                .contains("boundary"));
        Assert.assertEquals(
                "request-1",
                detail(resolution).getString("requestId"));
    }

    /**
     * 验证缺少 metadata Part 时不会再返回 ID 为空。
     */
    @Test
    public void shouldExplainMissingMetadataPart() {
        Resolution resolution = resolve(
                "multipart/form-data; boundary=test",
                new MissingServletRequestPartException("metadata"));

        Assert.assertEquals(400, resolution.response.getStatus());
        Assert.assertEquals(
                40013,
                resolution.modelAndView.getModel().get("errorCode"));
        Assert.assertTrue(String.valueOf(
                        resolution.modelAndView.getModel().get("message"))
                .contains("metadata Part"));
    }

    /**
     * 验证不支持的顶层媒体类型会同时提示两种合法模式。
     */
    @Test
    public void shouldExplainSupportedTopLevelMediaTypes() {
        Resolution resolution = resolve(
                MediaType.TEXT_PLAIN_VALUE,
                new HttpMediaTypeNotSupportedException(
                        MediaType.TEXT_PLAIN,
                        List.of(
                                MediaType.APPLICATION_JSON,
                                MediaType.MULTIPART_FORM_DATA)));

        Assert.assertEquals(415, resolution.response.getStatus());
        Assert.assertEquals(
                41501,
                resolution.modelAndView.getModel().get("errorCode"));
        Assert.assertTrue(String.valueOf(
                        resolution.modelAndView.getModel().get("message"))
                .contains("application/json"));
        Assert.assertEquals(
                MediaType.TEXT_PLAIN_VALUE,
                detail(resolution).getString("actual"));
    }

    /**
     * 验证旧发布门禁异常统一转换为不可枚举的 40401。
     */
    @Test
    public void shouldNormalizeLegacyUnpublishedWorkflowError() {
        Resolution resolution = resolve(
                MediaType.APPLICATION_JSON_VALUE,
                new BusinessException("工作流尚未发布"));

        Assert.assertEquals(404, resolution.response.getStatus());
        Assert.assertEquals(
                40401,
                resolution.modelAndView.getModel().get("errorCode"));
        Assert.assertEquals(
                "工作流不存在或当前不可公开调用",
                resolution.modelAndView.getModel().get("message"));
    }

    /**
     * 验证服务端业务异常不会把内部依赖详情返回调用方。
     */
    @Test
    public void shouldHideInternalBusinessErrorDetails() {
        Resolution resolution = resolve(
                MediaType.APPLICATION_JSON_VALUE,
                new BusinessException(
                        500,
                        50001,
                        "minio endpoint=http://internal:9000 signature=secret"));

        Assert.assertEquals(500, resolution.response.getStatus());
        Assert.assertEquals(
                "服务暂时不可用，请稍后重试",
                resolution.modelAndView.getModel().get("message"));
    }

    /**
     * 验证状态查询接口沿用工作流 Public API 稳定鉴权错误码。
     */
    @Test
    public void shouldNormalizeStatusApiAuthenticationError() {
        Resolution resolution = resolve(
                "/public-api/workflow/getChainStatus",
                MediaType.APPLICATION_JSON_VALUE,
                new BusinessException(
                        401,
                        401,
                        "apiKey 已过期"));

        Assert.assertEquals(401, resolution.response.getStatus());
        Assert.assertEquals(
                40103,
                resolution.modelAndView.getModel().get("errorCode"));
    }

    /**
     * 验证工作流执行状态缺失不会被误判为工作流资源缺失。
     */
    @Test
    public void shouldNormalizeMissingExecutionStateError() {
        Resolution resolution = resolve(
                "/public-api/workflow/getChainStatus",
                MediaType.APPLICATION_JSON_VALUE,
                new BusinessException(
                        404,
                        404,
                        "工作流执行状态不存在或已过期"));

        Assert.assertEquals(404, resolution.response.getStatus());
        Assert.assertEquals(
                40402,
                resolution.modelAndView.getModel().get("errorCode"));
        Assert.assertEquals(
                "执行记录不存在、已过期或不可访问",
                resolution.modelAndView.getModel().get("message"));
    }

    /**
     * 验证 API Key 无效和两层权限错误保持可区分。
     */
    @Test
    public void shouldKeepAuthenticationAndAuthorizationCodesDistinct() {
        assertBusinessCode(
                new BusinessException(
                        401,
                        401,
                        "apiKey 不存在或已禁用"),
                401,
                40102);
        assertBusinessCode(
                new BusinessException(
                        403,
                        403,
                        "该apiKey无权限访问该接口"),
                403,
                40301);
        assertBusinessCode(
                new BusinessException(
                        403,
                        403,
                        "该apiKey无权限调用工作流 API"),
                403,
                40302);
    }

    /**
     * 验证 Multipart 请求超限返回 41301。
     */
    @Test
    public void shouldTranslateMultipartUploadLimit() {
        Resolution resolution = resolve(
                "multipart/form-data; boundary=test",
                new MaxUploadSizeExceededException(1024L));

        Assert.assertEquals(413, resolution.response.getStatus());
        Assert.assertEquals(
                41301,
                resolution.modelAndView.getModel().get("errorCode"));
    }

    /**
     * 验证状态查询中的未知异常也统一返回安全 50001。
     */
    @Test
    public void shouldHideUnknownStatusApiFailure() {
        Resolution resolution = resolve(
                "/public-api/workflow/getChainStatus",
                MediaType.APPLICATION_JSON_VALUE,
                new IllegalStateException(
                        "redis endpoint=internal password=secret"));

        Assert.assertEquals(500, resolution.response.getStatus());
        Assert.assertEquals(
                50001,
                resolution.modelAndView.getModel().get("errorCode"));
        Assert.assertEquals(
                "服务暂时不可用，请稍后重试",
                resolution.modelAndView.getModel().get("message"));
        Assert.assertEquals(
                "request-1",
                detail(resolution).getString("requestId"));
    }

    /**
     * 验证单个旧业务异常的稳定状态与错误码。
     *
     * @param exception 旧业务异常
     * @param expectedStatus 期望 HTTP 状态
     * @param expectedCode 期望业务码
     */
    private void assertBusinessCode(
            BusinessException exception,
            int expectedStatus,
            int expectedCode) {
        Resolution resolution = resolve(
                "/public-api/workflow/getChainStatus",
                MediaType.APPLICATION_JSON_VALUE,
                exception);
        Assert.assertEquals(
                expectedStatus,
                resolution.response.getStatus());
        Assert.assertEquals(
                expectedCode,
                resolution.modelAndView.getModel().get("errorCode"));
    }

    /**
     * 执行异常解析并返回响应模型。
     *
     * @param contentType 顶层媒体类型
     * @param exception 原始异常
     * @return 解析结果
     */
    private Resolution resolve(
            String contentType,
            Exception exception) {
        return resolve(
                "/public-api/workflow/runAsync",
                contentType,
                exception);
    }

    /**
     * 执行指定工作流接口的异常解析。
     *
     * @param uri 请求地址
     * @param contentType 顶层媒体类型
     * @param exception 原始异常
     * @return 解析结果
     */
    private Resolution resolve(
            String uri,
            String contentType,
            Exception exception) {
        MockHttpServletRequest request = new MockHttpServletRequest(
                "POST",
                uri);
        request.setContentType(contentType);
        request.setAttribute(
                RequestIdContext.ATTRIBUTE_NAME,
                "request-1");
        request.setAttribute(
                RequestErrorProfile.ATTRIBUTE_NAME,
                WorkflowRunAsyncErrorProfile.INSTANCE);
        MockHttpServletResponse response =
                new MockHttpServletResponse();
        ModelAndView modelAndView = resolver.resolveException(
                request,
                response,
                this,
                exception);
        return new Resolution(response, modelAndView);
    }

    /**
     * 获取错误详情 JSON。
     *
     * @param resolution 解析结果
     * @return 错误详情
     */
    private JSONObject detail(Resolution resolution) {
        Object data = resolution.modelAndView.getModel().get("data");
        return data instanceof JSONObject object
                ? object
                : JSONObject.parseObject(String.valueOf(data));
    }

    /**
     * 错误解析结果。
     *
     * @param response HTTP 响应
     * @param modelAndView JSON 视图模型
     */
    private record Resolution(
            MockHttpServletResponse response,
            ModelAndView modelAndView) {
    }
}
