package tech.easyflow.admin.controller.skill;

import cn.dev33.satoken.annotation.SaCheckPermission;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.testng.Assert;
import org.testng.annotations.Test;
import tech.easyflow.admin.controller.ai.support.AiResourceCreatorNameSupport;
import tech.easyflow.admin.controller.skill.vo.SkillCopyRequest;
import tech.easyflow.admin.controller.skill.vo.SkillDraftRequest;
import tech.easyflow.admin.controller.skill.vo.SkillGitRepositoryPrepareRequest;
import tech.easyflow.admin.controller.skill.vo.SkillGitRepositoryScanRequest;
import tech.easyflow.admin.controller.skill.vo.SkillImportBatchResultView;
import tech.easyflow.admin.controller.skill.vo.SkillToolBindingUpdateRequest;
import tech.easyflow.admin.controller.skill.vo.SkillView;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.approval.entity.vo.ApprovalActionResult;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import tech.easyflow.common.web.jsonbody.JsonBodyParser;
import tech.easyflow.skill.entity.Skill;
import tech.easyflow.skill.file.SkillFileService;
import tech.easyflow.skill.gitimport.SkillGitImportService;
import tech.easyflow.skill.imports.SkillExportService;
import tech.easyflow.skill.imports.SkillImportConfirmRequest;
import tech.easyflow.skill.imports.SkillImportService;
import tech.easyflow.skill.publish.SkillPublishAppService;
import tech.easyflow.skill.security.SkillVisibilityQueryHelper;
import tech.easyflow.skill.service.SkillApprovalStateService;
import tech.easyflow.skill.service.SkillService;
import tech.easyflow.skill.service.SkillToolBindingService;
import tech.easyflow.skill.service.SkillToolOptionQueryService;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.ResourceAccessService;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Modifier;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * {@link SkillController} 标准包管理 API 契约测试。
 */
public class SkillControllerContractTest {

    /**
     * Tool 绑定请求能够把嵌套列表转换为白名单 JavaBean。
     *
     * @throws Exception 反序列化失败
     */
    @Test
    public void jsonBodyParserDeserializesNestedToolBindings() throws Exception {
        JSONObject json = JSON.parseObject("""
                {
                  "skillId": 101,
                  "bindings": [{
                    "toolType": "WORKFLOW",
                    "targetId": 202,
                    "hitlEnabled": true,
                    "sortNo": 0,
                    "resourceSnapshot": {"forged": true}
                  }]
                }
                """);

        SkillToolBindingUpdateRequest request = (SkillToolBindingUpdateRequest) JsonBodyParser.parseJsonBody(
                json, SkillToolBindingUpdateRequest.class, SkillToolBindingUpdateRequest.class, "");

        Assert.assertEquals(request.getSkillId(), BigInteger.valueOf(101));
        Assert.assertEquals(request.getBindings().get(0).getTargetId(), BigInteger.valueOf(202));
        Assert.assertEquals(request.getBindings().get(0).getToolType(), "WORKFLOW");
        Assert.assertTrue(request.getBindings().get(0).getHitlEnabled());
        Assert.assertEquals(
                Arrays.stream(SkillToolBindingUpdateRequest.Binding.class.getDeclaredFields())
                        .filter(field -> !Modifier.isStatic(field.getModifiers()))
                        .map(field -> field.getName()).toList(),
                List.of("toolType", "targetId", "hitlEnabled", "sortNo", "mcpToolManifestHash"));
    }

    /**
     * 草稿白名单 DTO 只接受标准包治理字段。
     *
     * @throws Exception 反序列化失败
     */
    @Test
    public void jsonBodyParserDeserializesStandardDraftRecord() throws Exception {
        JSONObject json = JSON.parseObject("""
                {
                  "id": 101,
                  "categoryId": 9,
                  "displayName": "演示 Skill",
                  "skillContent": "---\nname: demo-skill\ndescription: Demo\n---\n# Demo\n",
                  "visibilityScope": "PRIVATE"
                }
                """);

        SkillDraftRequest request = (SkillDraftRequest) JsonBodyParser.parseJsonBody(
                json, SkillDraftRequest.class, SkillDraftRequest.class, "");

        Assert.assertEquals(request.id(), BigInteger.valueOf(101));
        Assert.assertEquals(request.categoryId(), BigInteger.valueOf(9));
        Assert.assertEquals(request.displayName(), "演示 Skill");
        Assert.assertEquals(request.visibilityScope(), "PRIVATE");
        Assert.assertFalse(Arrays.stream(SkillDraftRequest.class.getRecordComponents())
                .anyMatch(component -> "enabled".equals(component.getName())));
    }

    /**
     * 保存入口使用白名单 DTO 并返回 Skill 视图。
     *
     * @throws Exception 反射失败
     */
    @Test
    public void saveEndpointUsesDraftRequestAndSkillView() throws Exception {
        Method method = SkillController.class.getMethod("save", SkillDraftRequest.class);
        JsonBody jsonBody = method.getParameters()[0].getAnnotation(JsonBody.class);
        ParameterizedType returnType = (ParameterizedType) method.getGenericReturnType();

        Assert.assertNotNull(jsonBody);
        Assert.assertEquals(returnType.getRawType(), Result.class);
        Assert.assertEquals(returnType.getActualTypeArguments()[0], SkillView.class);
    }

    /**
     * 复制只依赖 Skill 新建权限，不再暴露能力绑定权限。
     *
     * @throws Exception 反射失败
     */
    @Test
    public void copyEndpointUsesOnlySavePermission() throws Exception {
        SaCheckPermission permission = SkillController.class
                .getMethod("copy", SkillCopyRequest.class).getAnnotation(SaCheckPermission.class);

        Assert.assertEquals(permission.value(), new String[]{"/api/v1/skill/save"});
        Assert.assertFalse(Arrays.stream(SkillController.class.getDeclaredMethods())
                .map(method -> method.getAnnotation(SaCheckPermission.class))
                .filter(java.util.Objects::nonNull)
                .flatMap(item -> Arrays.stream(item.value()))
                .anyMatch("/api/v1/skill/capability"::equals));
    }

    /**
     * Git 仓库扫描与候选准备沿用 Skill 导入权限，并使用白名单请求 DTO。
     *
     * @throws Exception 反射失败
     */
    @Test
    public void gitRepositoryEndpointsReuseImportPermission() throws Exception {
        Method scan = SkillController.class.getMethod(
                "scanGitRepository", SkillGitRepositoryScanRequest.class);
        Method prepare = SkillController.class.getMethod(
                "prepareGitRepositoryImport", SkillGitRepositoryPrepareRequest.class);

        Assert.assertEquals(scan.getAnnotation(SaCheckPermission.class).value(),
                new String[]{"/api/v1/skill/import"});
        Assert.assertEquals(prepare.getAnnotation(SaCheckPermission.class).value(),
                new String[]{"/api/v1/skill/import"});
        Assert.assertNotNull(scan.getParameters()[0].getAnnotation(JsonBody.class));
        Assert.assertNotNull(prepare.getParameters()[0].getAnnotation(JsonBody.class));
    }

    /**
     * 发布入口把必填发布说明原样交给应用服务。
     */
    @Test
    public void publishEndpointForwardsRequiredReason() {
        BigInteger id = BigInteger.valueOf(101);
        SkillPublishAppService publishService = mock(SkillPublishAppService.class);
        when(publishService.submitPublishApproval(id, "补充审核规则"))
                .thenReturn(ApprovalActionResult.direct());
        SkillController controller = controller(mock(SkillImportService.class), publishService);

        controller.submitPublishApproval(id, "补充审核规则");

        verify(publishService).submitPublishApproval(id, "补充审核规则");
    }

    /**
     * 批量确认按 token 隔离业务失败，成功项仍保留结果。
     */
    @Test
    public void batchConfirmKeepsIndependentResults() {
        SkillImportService importService = mock(SkillImportService.class);
        SkillImportConfirmRequest first = request("a".repeat(32));
        SkillImportConfirmRequest second = request("b".repeat(32));
        when(importService.confirm(first)).thenThrow(new BusinessException("名称不可用"));
        Skill imported = new Skill();
        imported.setId(BigInteger.ONE);
        imported.setName("demo-skill");
        imported.setPublishStatus(PublishStatus.DRAFT.getCode());
        when(importService.confirm(second)).thenReturn(List.of(imported));

        List<SkillImportBatchResultView> results = controller(importService, mock(SkillPublishAppService.class))
                .importConfirmBatch(List.of(first, second)).getData();

        Assert.assertFalse(results.get(0).success());
        Assert.assertEquals(results.get(0).message(), "名称不可用");
        Assert.assertTrue(results.get(1).success());
        Assert.assertEquals(results.get(1).skills().get(0).name(), "demo-skill");
    }

    private SkillImportConfirmRequest request(String token) {
        SkillImportConfirmRequest request = new SkillImportConfirmRequest();
        request.setImportToken(token);
        request.setVisibilityScope("PRIVATE");
        return request;
    }

    private SkillController controller(SkillImportService importService, SkillPublishAppService publishService) {
        ResourceAccessService accessService = mock(ResourceAccessService.class);
        when(accessService.canAccess(any(), any(), any())).thenReturn(true);
        return new SkillController(mock(SkillService.class), mock(SkillApprovalStateService.class),
                publishService, importService, mock(SkillGitImportService.class),
                mock(SkillExportService.class), mock(SkillFileService.class),
                mock(SkillToolBindingService.class), mock(SkillToolOptionQueryService.class),
                accessService, mock(CategoryPermissionService.class), mock(SkillVisibilityQueryHelper.class),
                mock(AiResourceCreatorNameSupport.class));
    }
}
