package tech.easyflow.agent.runtime.skill;

import com.easyagents.agent.runtime.mcp.McpToolManifestEntry;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.InOrder;
import tech.easyflow.agent.entity.Agent;
import tech.easyflow.agent.entity.AgentSkillBinding;
import tech.easyflow.agent.service.AgentDependencyAccessService;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.skill.entity.Skill;
import tech.easyflow.skill.service.SkillService;

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

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

/**
 * Agent Skill 冻结运行投影测试。
 */
public class AgentSkillRuntimeProjectorTest {

    /**
     * 投影应只使用 Skill 发布快照展示字段、排除二进制正文并校验组合 hash。
     */
    @Test
    @SuppressWarnings("unchecked")
    public void shouldProjectPublishedContentAndDetectTampering() {
        AgentDependencyAccessService accessService = mock(AgentDependencyAccessService.class);
        SkillService skillService = mock(SkillService.class);
        AgentSkillRuntimeProjector projector = new AgentSkillRuntimeProjector(
                accessService, skillService, new ObjectMapper());
        Skill skill = skill(BigInteger.ONE, "线上名称");
        skill.setDisplayName("未发布草稿名称");
        when(accessService.requireSkill(org.mockito.ArgumentMatchers.any(),
                org.mockito.ArgumentMatchers.eq(BigInteger.ONE))).thenReturn(skill);

        List<AgentSkillBinding> projected = projector.projectCurrentBindings(
                new Agent(), List.of(binding(BigInteger.ONE)));

        Map<String, Object> snapshot = projected.get(0).getResourceSnapshot();
        Map<String, Object> summary = projected.get(0).getResourceSummary();
        Assert.assertEquals("线上名称", snapshot.get("displayName"));
        Assert.assertEquals(Map.of("references/guide.md", "指南正文"), snapshot.get("resources"));
        Assert.assertEquals(1, summary.get("binaryExcludedCount"));
        Assert.assertEquals(1, summary.get("toolCount"));
        projector.assertFrozenBindings(projected);

        Map<String, Object> tampered = new LinkedHashMap<>(snapshot);
        tampered.put("skillContent", "被篡改的指令");
        projected.get(0).setResourceSnapshot(tampered);
        Assert.assertThrows(BusinessException.class, () -> projector.assertFrozenBindings(projected));
    }

    /**
     * MCP 清单在发布前为 POJO、落库后为 Map 时，运行快照 hash 应保持一致。
     */
    @Test
    @SuppressWarnings("unchecked")
    public void shouldKeepRuntimeHashAfterMcpManifestJsonRoundTrip() {
        ObjectMapper objectMapper = new ObjectMapper();
        AgentDependencyAccessService accessService = mock(AgentDependencyAccessService.class);
        SkillService skillService = mock(SkillService.class);
        AgentSkillRuntimeProjector projector = new AgentSkillRuntimeProjector(
                accessService, skillService, objectMapper);
        Skill skill = skill(BigInteger.ONE, "MCP Skill");
        McpToolManifestEntry entry = new McpToolManifestEntry();
        entry.setName("query-docs");
        entry.setDescription("查询文档");
        entry.setInputSchema(Map.of("type", "object"));
        entry.setOutputSchema(Map.of("type", "object"));
        skill.setPublishedToolBindingsJson(Map.of(
                "schemaVersion", 1,
                "bindings", List.of(Map.of(
                        "toolType", "MCP",
                        "targetId", 200,
                        "toolCount", 1,
                        "mcpToolManifest", List.of(entry),
                        "resourceSnapshot", Map.of("id", 200))),
                "snapshotHash", "tools-mcp"));
        when(accessService.requireSkill(org.mockito.ArgumentMatchers.any(),
                org.mockito.ArgumentMatchers.eq(BigInteger.ONE))).thenReturn(skill);

        List<AgentSkillBinding> projected = projector.projectCurrentBindings(
                new Agent(), List.of(binding(BigInteger.ONE)));
        Map<String, Object> persisted = objectMapper.convertValue(
                projected.get(0).getResourceSnapshot(), Map.class);
        projected.get(0).setResourceSnapshot(persisted);

        projector.assertFrozenBindings(projected);
    }

    /**
     * 资源锁应按 Skill ID 稳定获取，同时保留用户编排顺序。
     */
    @Test
    public void shouldLockSkillsInStableOrderAndPreserveBindingOrder() {
        AgentDependencyAccessService accessService = mock(AgentDependencyAccessService.class);
        SkillService skillService = mock(SkillService.class);
        AgentSkillRuntimeProjector projector = new AgentSkillRuntimeProjector(
                accessService, skillService, new ObjectMapper());
        Agent agent = new Agent();
        when(accessService.requireSkill(agent, BigInteger.ONE)).thenReturn(skill(BigInteger.ONE, "一"));
        when(accessService.requireSkill(agent, BigInteger.TWO)).thenReturn(skill(BigInteger.TWO, "二"));

        List<AgentSkillBinding> projected = projector.projectCurrentBindings(
                agent, List.of(binding(BigInteger.TWO), binding(BigInteger.ONE)));

        InOrder order = inOrder(accessService);
        order.verify(accessService).requireSkill(agent, BigInteger.ONE);
        order.verify(accessService).requireSkill(agent, BigInteger.TWO);
        Assert.assertEquals(BigInteger.TWO, projected.get(0).getSkillId());
        Assert.assertEquals(BigInteger.ONE, projected.get(1).getSkillId());
    }

    /**
     * 同一 Agent 不得重复绑定同一 Skill。
     */
    @Test
    public void shouldRejectDuplicateSkillBindings() {
        AgentDependencyAccessService accessService = mock(AgentDependencyAccessService.class);
        SkillService skillService = mock(SkillService.class);
        AgentSkillRuntimeProjector projector = new AgentSkillRuntimeProjector(
                accessService, skillService, new ObjectMapper());
        when(accessService.requireSkill(org.mockito.ArgumentMatchers.any(),
                org.mockito.ArgumentMatchers.eq(BigInteger.ONE))).thenReturn(skill(BigInteger.ONE, "一"));

        Assert.assertThrows(BusinessException.class, () -> projector.projectCurrentBindings(
                new Agent(), List.of(binding(BigInteger.ONE), binding(BigInteger.ONE))));
    }

    /**
     * 单个 Agent 超过二十个 Skill 时应在访问依赖资源前拒绝。
     */
    @Test
    public void shouldRejectMoreThanTwentySkills() {
        AgentSkillRuntimeProjector projector = new AgentSkillRuntimeProjector(
                mock(AgentDependencyAccessService.class), mock(SkillService.class), new ObjectMapper());
        List<AgentSkillBinding> bindings = new ArrayList<>();
        for (int index = 1; index <= 21; index++) {
            bindings.add(binding(BigInteger.valueOf(index)));
        }

        Assert.assertThrows(BusinessException.class,
                () -> projector.projectCurrentBindings(new Agent(), bindings));
    }

    /**
     * 创建带发布内容和 Tool 快照的 Skill。
     *
     * @param id Skill ID
     * @param publishedDisplayName 已发布展示名
     * @return Skill
     */
    private Skill skill(BigInteger id, String publishedDisplayName) {
        Skill skill = new Skill();
        skill.setId(id);
        skill.setName("skill-" + id);
        skill.setDisplayName(publishedDisplayName);
        Map<String, Object> content = new LinkedHashMap<>();
        content.put("schemaVersion", 2);
        content.put("name", "skill-" + id);
        content.put("displayName", publishedDisplayName);
        content.put("description", "用于测试 Skill 运行投影");
        content.put("visibilityScope", "PRIVATE");
        content.put("skillContent", "# 指令\n执行测试");
        content.put("packageHash", "package-" + id);
        content.put("resources", List.of(
                Map.of("path", "references/guide.md", "text", true, "textContent", "指南正文"),
                Map.of("path", "assets/template.bin", "text", false, "contentRef", "sha256:binary")));
        content.put("snapshotHash", "content-" + id);
        skill.setPublishedSnapshotJson(content);
        skill.setPublishedToolBindingsJson(Map.of(
                "schemaVersion", 1,
                "bindings", List.of(Map.of(
                        "toolType", "WORKFLOW",
                        "targetId", 100,
                        "toolCount", 1,
                        "resourceSnapshot", Map.of("id", 100))),
                "snapshotHash", "tools-" + id));
        return skill;
    }

    /**
     * 创建 Skill 绑定。
     *
     * @param skillId Skill ID
     * @return 绑定
     */
    private AgentSkillBinding binding(BigInteger skillId) {
        AgentSkillBinding binding = new AgentSkillBinding();
        binding.setSkillId(skillId);
        return binding;
    }
}
