package tech.easyflow.skill.service.impl;

import com.easyagents.agent.runtime.mcp.McpToolManifestEntry;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.entity.Mcp;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.skill.entity.Skill;
import tech.easyflow.skill.entity.SkillToolBinding;
import tech.easyflow.skill.mapper.SkillMapper;
import tech.easyflow.skill.service.SkillToolResourceService;
import tech.easyflow.system.service.ResourceAccessService;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

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

/**
 * Skill Tool 绑定发布快照测试。
 */
public class SkillToolBindingServiceImplTest {

    /**
     * 发布快照应包含资源冻结数据，并拒绝任何后续篡改。
     */
    @Test
    public void shouldBuildAndVerifyFrozenToolSnapshot() {
        SkillToolResourceService resources = mock(SkillToolResourceService.class);
        SkillToolBinding workflowBinding = binding("WORKFLOW", 100, null);
        Workflow workflow = new Workflow();
        workflow.setId(BigInteger.valueOf(100));
        workflow.setTitle("合同审查流程");
        when(resources.requireWorkflow(org.mockito.ArgumentMatchers.any(),
                org.mockito.ArgumentMatchers.eq(workflowBinding))).thenReturn(workflow);
        when(resources.snapshotWorkflow(workflow)).thenReturn(Map.of(
                "id", BigInteger.valueOf(100),
                "content", "{\"nodes\":[]}"));
        SkillToolBindingServiceImpl service = service(resources, List.of(workflowBinding));

        Map<String, Object> snapshot = service.buildPublishSnapshot(skill());

        service.assertPublishedSnapshotHash(snapshot);
        verify(resources).requireWorkflow(org.mockito.ArgumentMatchers.any(),
                org.mockito.ArgumentMatchers.eq(workflowBinding));
        Map<String, Object> tampered = new LinkedHashMap<>(snapshot);
        tampered.put("schemaVersion", 2);
        Assert.assertThrows(BusinessException.class,
                () -> service.assertPublishedSnapshotHash(tampered));
    }

    /**
     * 发布快照经过数据库 JSON 持久化后仍应保持同一 hash。
     *
     * @throws Exception JSON 往返失败时抛出
     */
    @Test
    @SuppressWarnings("unchecked")
    public void shouldVerifyMcpSnapshotAfterJsonRoundTrip() throws Exception {
        SkillToolResourceService resources = mock(SkillToolResourceService.class);
        SkillToolBinding mcpBinding = binding("MCP", 200, "manifest-hash");
        Mcp mcp = mcp();
        when(resources.requireMcp(org.mockito.ArgumentMatchers.any(),
                org.mockito.ArgumentMatchers.eq(mcpBinding))).thenReturn(new SkillToolResourceService.McpResource(
                mcp, List.of(manifest("search")), "manifest-hash"));
        when(resources.snapshotMcpConnection(mcp)).thenReturn(Map.of(
                "id", mcp.getId(),
                "configJson", "{\"mcpServers\":{}}"));
        SkillToolBindingServiceImpl service = service(resources, List.of(mcpBinding));
        ObjectMapper mapper = new ObjectMapper();

        Map<String, Object> snapshot = service.buildPublishSnapshot(skill());
        Map<String, Object> persisted = mapper.readValue(
                mapper.writeValueAsBytes(snapshot), Map.class);

        service.assertPublishedSnapshotHash(persisted);
    }

    /**
     * MCP 清单变化后发布必须失败，要求用户重新保存并确认绑定。
     */
    @Test
    public void shouldRejectChangedMcpManifestAtPublish() {
        SkillToolResourceService resources = mock(SkillToolResourceService.class);
        SkillToolBinding mcpBinding = binding("MCP", 200, "old-hash");
        when(resources.requireMcp(org.mockito.ArgumentMatchers.any(),
                org.mockito.ArgumentMatchers.eq(mcpBinding))).thenReturn(new SkillToolResourceService.McpResource(
                mcp(), List.of(manifest("search")), "new-hash"));
        SkillToolBindingServiceImpl service = service(resources, List.of(mcpBinding));

        BusinessException exception = Assert.assertThrows(BusinessException.class,
                () -> service.buildPublishSnapshot(skill()));

        Assert.assertTrue(exception.getMessage().contains("清单已变化"));
    }

    /**
     * 单个 Skill 展开后的实际 MCP Tool 数量不得超过二十个。
     */
    @Test
    public void shouldRejectMoreThanTwentyExpandedTools() {
        SkillToolResourceService resources = mock(SkillToolResourceService.class);
        SkillToolBinding mcpBinding = binding("MCP", 200, "manifest-hash");
        List<McpToolManifestEntry> manifest = new ArrayList<>();
        for (int index = 1; index <= 21; index++) {
            manifest.add(manifest("tool-" + index));
        }
        when(resources.requireMcp(org.mockito.ArgumentMatchers.any(),
                org.mockito.ArgumentMatchers.eq(mcpBinding))).thenReturn(new SkillToolResourceService.McpResource(
                mcp(), manifest, "manifest-hash"));
        SkillToolBindingServiceImpl service = service(resources, List.of(mcpBinding));

        BusinessException exception = Assert.assertThrows(BusinessException.class,
                () -> service.buildPublishSnapshot(skill()));

        Assert.assertTrue(exception.getMessage().contains("20"));
        verify(resources).requireMcp(org.mockito.ArgumentMatchers.any(),
                org.mockito.ArgumentMatchers.eq(mcpBinding));
    }

    /**
     * 创建可注入固定绑定列表的服务。
     *
     * @param resources Tool 资源服务
     * @param bindings 固定绑定
     * @return 测试服务
     */
    private SkillToolBindingServiceImpl service(SkillToolResourceService resources,
                                                List<SkillToolBinding> bindings) {
        return new SkillToolBindingServiceImpl(
                mock(SkillMapper.class), resources, mock(ResourceAccessService.class), new ObjectMapper()) {
            @Override
            public List<SkillToolBinding> listBindings(BigInteger skillId) {
                return bindings;
            }
        };
    }

    /**
     * 创建测试 Skill。
     *
     * @return Skill
     */
    private Skill skill() {
        Skill skill = new Skill();
        skill.setId(BigInteger.ONE);
        skill.setTenantId(BigInteger.ONE);
        return skill;
    }

    /**
     * 创建 Tool 绑定。
     *
     * @param type 类型
     * @param targetId 目标 ID
     * @param manifestHash MCP 清单 hash
     * @return 绑定
     */
    private SkillToolBinding binding(String type, long targetId, String manifestHash) {
        SkillToolBinding binding = new SkillToolBinding();
        binding.setToolType(type);
        binding.setTargetId(BigInteger.valueOf(targetId));
        binding.setMcpToolManifestHash(manifestHash);
        binding.setSortNo(0);
        return binding;
    }

    /**
     * 创建测试 MCP。
     *
     * @return MCP
     */
    private Mcp mcp() {
        Mcp mcp = new Mcp();
        mcp.setId(BigInteger.valueOf(200));
        mcp.setTitle("测试 MCP");
        return mcp;
    }

    /**
     * 创建最小 MCP Tool 清单项。
     *
     * @param name Tool 名称
     * @return 清单项
     */
    private McpToolManifestEntry manifest(String name) {
        McpToolManifestEntry entry = new McpToolManifestEntry();
        entry.setName(name);
        entry.setDescription("测试工具");
        entry.setInputSchema(Map.of("type", "object"));
        return entry;
    }
}
