package tech.easyflow.ai.service.impl;

import com.easyagents.core.model.chat.tool.Tool;
import com.easyagents.rag.retrieval.RetrievalMode;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.chattime.availability.ChatTimeKnowledgeAvailabilityResolver;
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityContext;
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityService;
import tech.easyflow.ai.chattime.availability.ChatTimeToolAvailabilityServiceImpl;
import tech.easyflow.ai.easyagents.tool.DocumentCollectionTool;
import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.entity.BotDocumentCollection;
import tech.easyflow.ai.entity.DocumentCollection;
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.system.entity.vo.RoleCategoryAccessSnapshot;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.SysDeptService;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * {@link BotServiceImpl} 单元测试。
 *
 * @author Codex
 * @since 2026-05-10
 */
public class BotServiceImplTest {

    /**
     * 仅应为当前用户可访问的知识库生成聊天工具，并保留绑定检索模式。
     *
     * @throws Exception 反射注入异常
     */
    @Test
    public void buildKnowledgeToolsShouldOnlyCreateAvailableTools() throws Exception {
        BotServiceImpl service = new BotServiceImpl();
        injectAvailabilityService(service, buildAvailabilityService(
                new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(21))),
                Collections.emptySet()
        ));

        List<Tool> tools = service.buildKnowledgeTools(
                List.of(
                        buildBinding(101, 11, 21, "PUBLIC", RetrievalMode.KEYWORD),
                        buildBinding(102, 11, 99, "PUBLIC", RetrievalMode.HYBRID)
                ),
                false,
                buildContext(12, 3)
        );

        Assert.assertEquals(1, tools.size());
        Assert.assertTrue(tools.get(0) instanceof DocumentCollectionTool);
        DocumentCollectionTool tool = (DocumentCollectionTool) tools.get(0);
        Assert.assertEquals(BigInteger.valueOf(101), tool.getKnowledgeId());
        Assert.assertEquals(RetrievalMode.KEYWORD, tool.getRetrievalMode());
        Assert.assertNotNull(tool.getChatTimeContext());
    }

    /**
     * 当全部绑定知识库都不可用时，聊天工具列表应为空。
     *
     * @throws Exception 反射注入异常
     */
    @Test
    public void buildKnowledgeToolsShouldReturnEmptyWhenAllBindingsUnavailable() throws Exception {
        BotServiceImpl service = new BotServiceImpl();
        injectAvailabilityService(service, buildAvailabilityService(
                new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(99))),
                Collections.emptySet()
        ));

        List<Tool> tools = service.buildKnowledgeTools(
                List.of(buildBinding(101, 11, 21, "PUBLIC", RetrievalMode.HYBRID)),
                false,
                buildContext(12, 3)
        );

        Assert.assertTrue(tools.isEmpty());
    }

    /**
     * 已发布快照分支也应按聊天态权限过滤知识库，并保留检索模式。
     *
     * @throws Exception 反射调用异常
     */
    @Test
    @SuppressWarnings("unchecked")
    public void appendPublishedKnowledgeToolsShouldFilterUnavailableBindings() throws Exception {
        BotServiceImpl service = new BotServiceImpl();
        injectAvailabilityService(service, buildAvailabilityService(
                new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(21))),
                Collections.emptySet()
        ));
        injectDocumentCollectionService(service, mockDocumentCollectionService(
                buildKnowledge(101, 11, 21, "PUBLIC"),
                buildKnowledge(102, 11, 99, "PUBLIC")
        ));

        Bot runtimeBot = new Bot();
        Map<String, Object> snapshot = new HashMap<>();
        snapshot.put("knowledgeBindings", List.of(
                buildPublishedBinding(101, RetrievalMode.KEYWORD),
                buildPublishedBinding(102, RetrievalMode.HYBRID)
        ));
        runtimeBot.setPublishedSnapshotJson(snapshot);
        List<Tool> functionList = new ArrayList<>();
        Method method = BotServiceImpl.class.getDeclaredMethod(
                "appendPublishedKnowledgeTools",
                List.class,
                Bot.class,
                boolean.class,
                ChatTimeToolAvailabilityContext.class,
                Set.class
        );
        method.setAccessible(true);

        method.invoke(service, functionList, runtimeBot, false, buildContext(12, 3), new LinkedHashSet<>());

        Assert.assertEquals(1, functionList.size());
        DocumentCollectionTool tool = (DocumentCollectionTool) functionList.get(0);
        Assert.assertEquals(BigInteger.valueOf(101), tool.getKnowledgeId());
        Assert.assertEquals(RetrievalMode.KEYWORD, tool.getRetrievalMode());
    }

    /**
     * 额外知识库应按用户选择顺序去重，并限制最多 3 个。
     *
     * @throws Exception 反射调用异常
     */
    @Test
    @SuppressWarnings("unchecked")
    public void sanitizeExtraKnowledgeIdsShouldDeduplicateAndKeepOrder() throws Exception {
        BotServiceImpl service = new BotServiceImpl();
        Method method = BotServiceImpl.class.getDeclaredMethod("sanitizeExtraKnowledgeIds", List.class);
        method.setAccessible(true);

        List<BigInteger> result = (List<BigInteger>) method.invoke(
                service,
                List.of(
                        BigInteger.valueOf(3),
                        BigInteger.valueOf(1),
                        BigInteger.valueOf(3),
                        BigInteger.valueOf(2)
                )
        );

        Assert.assertEquals(
                List.of(BigInteger.valueOf(3), BigInteger.ONE, BigInteger.valueOf(2)),
                result
        );
    }

    /**
     * 额外知识库超过 3 个时应直接拒绝请求。
     *
     * @throws Exception 反射调用异常
     */
    @Test
    public void sanitizeExtraKnowledgeIdsShouldRejectWhenMoreThanThree() throws Exception {
        BotServiceImpl service = new BotServiceImpl();
        Method method = BotServiceImpl.class.getDeclaredMethod("sanitizeExtraKnowledgeIds", List.class);
        method.setAccessible(true);

        try {
            method.invoke(
                    service,
                    List.of(
                            BigInteger.ONE,
                            BigInteger.valueOf(2),
                            BigInteger.valueOf(3),
                            BigInteger.valueOf(4)
                    )
            );
            Assert.fail("expected BusinessException");
        } catch (java.lang.reflect.InvocationTargetException exception) {
            Assert.assertTrue(exception.getTargetException() instanceof tech.easyflow.common.web.exceptions.BusinessException);
            Assert.assertEquals("额外知识库最多选择 3 个", exception.getTargetException().getMessage());
        }
    }

    /**
     * 额外知识库工具应强制使用发布态视图，并跳过重复选择。
     *
     * @throws Exception 反射注入异常
     */
    @Test
    public void appendExtraKnowledgeToolsShouldUsePublishedKnowledgeAndDeduplicate() throws Exception {
        BotServiceImpl service = new BotServiceImpl();
        injectAvailabilityService(service, buildAvailabilityService(
                new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(21))),
                Collections.emptySet()
        ));

        DocumentCollection draftKnowledge = buildKnowledge(101, 11, 21, "PUBLIC");
        draftKnowledge.setPublishStatus("PUBLISHED");
        draftKnowledge.setTitle("draft-title");
        draftKnowledge.setDescription("draft-desc");

        DocumentCollection publishedKnowledge = buildKnowledge(101, 11, 21, "PUBLIC");
        publishedKnowledge.setPublishStatus("PUBLISHED");
        publishedKnowledge.setTitle("published-title");
        publishedKnowledge.setDescription("published-desc");

        injectDocumentCollectionService(service, mockDocumentCollectionServiceForExtra(draftKnowledge, publishedKnowledge));

        List<Tool> tools = new ArrayList<>();
        service.appendExtraKnowledgeTools(
                tools,
                List.of(BigInteger.valueOf(101), BigInteger.valueOf(101)),
                false,
                buildContext(12, 3),
                new LinkedHashSet<>()
        );

        Assert.assertEquals(1, tools.size());
        DocumentCollectionTool tool = (DocumentCollectionTool) tools.get(0);
        Assert.assertEquals(BigInteger.valueOf(101), tool.getKnowledgeId());
        Assert.assertEquals("published-desc", tool.getDescription());
    }

    private void injectAvailabilityService(BotServiceImpl service, ChatTimeToolAvailabilityService availabilityService) throws Exception {
        Field field = BotServiceImpl.class.getDeclaredField("chatTimeToolAvailabilityService");
        field.setAccessible(true);
        field.set(service, availabilityService);
    }

    private void injectDocumentCollectionService(BotServiceImpl service, tech.easyflow.ai.service.DocumentCollectionService documentCollectionService) throws Exception {
        Field field = BotServiceImpl.class.getDeclaredField("documentCollectionService");
        field.setAccessible(true);
        field.set(service, documentCollectionService);
    }

    private ChatTimeToolAvailabilityService buildAvailabilityService(RoleCategoryAccessSnapshot accessSnapshot,
                                                                    Set<BigInteger> deptIds) {
        return new ChatTimeToolAvailabilityServiceImpl(List.of(
                new ChatTimeKnowledgeAvailabilityResolver(
                        new KnowledgeVisibilityQueryHelper(),
                        mockCategoryPermissionService(accessSnapshot),
                        mockSysDeptService(deptIds)
                )
        ));
    }

    private ChatTimeToolAvailabilityContext buildContext(long accountId, long deptId) {
        LoginAccount loginAccount = new LoginAccount();
        loginAccount.setId(BigInteger.valueOf(accountId));
        loginAccount.setDeptId(BigInteger.valueOf(deptId));
        ChatTimeToolAvailabilityContext context = new ChatTimeToolAvailabilityContext();
        context.setLoginAccount(loginAccount);
        return context;
    }

    private BotDocumentCollection buildBinding(long knowledgeId,
                                               long createdBy,
                                               long categoryId,
                                               String visibilityScope,
                                               RetrievalMode retrievalMode) {
        DocumentCollection knowledge = buildKnowledge(knowledgeId, createdBy, categoryId, visibilityScope);
        BotDocumentCollection binding = new BotDocumentCollection();
        binding.setDocumentCollectionId(BigInteger.valueOf(knowledgeId));
        binding.setKnowledge(knowledge);
        binding.setRetrievalMode(retrievalMode);
        return binding;
    }

    private DocumentCollection buildKnowledge(long knowledgeId,
                                              long createdBy,
                                              long categoryId,
                                              String visibilityScope) {
        DocumentCollection knowledge = new DocumentCollection();
        knowledge.setId(BigInteger.valueOf(knowledgeId));
        knowledge.setTitle("knowledge-" + knowledgeId);
        knowledge.setDescription("desc-" + knowledgeId);
        knowledge.setCreatedBy(BigInteger.valueOf(createdBy));
        knowledge.setCategoryId(BigInteger.valueOf(categoryId));
        knowledge.setVisibilityScope(visibilityScope);
        knowledge.setPublishStatus("PUBLISHED");
        return knowledge;
    }

    private Map<String, Object> buildPublishedBinding(long knowledgeId, RetrievalMode retrievalMode) {
        Map<String, Object> binding = new HashMap<>();
        binding.put("knowledgeId", String.valueOf(knowledgeId));
        binding.put("retrievalMode", retrievalMode.name());
        return binding;
    }

    private tech.easyflow.ai.service.DocumentCollectionService mockDocumentCollectionService(DocumentCollection... collections) {
        Map<BigInteger, DocumentCollection> knowledgeMap = new HashMap<>();
        for (DocumentCollection collection : collections) {
            knowledgeMap.put(collection.getId(), collection);
        }
        return (tech.easyflow.ai.service.DocumentCollectionService) Proxy.newProxyInstance(
                tech.easyflow.ai.service.DocumentCollectionService.class.getClassLoader(),
                new Class<?>[]{tech.easyflow.ai.service.DocumentCollectionService.class},
                (proxy, method, args) -> {
                    if ("getPublishedById".equals(method.getName()) && args != null && args.length == 1 && args[0] instanceof BigInteger knowledgeId) {
                        return knowledgeMap.get(knowledgeId);
                    }
                    return defaultValue(method.getReturnType());
                }
        );
    }

    private tech.easyflow.ai.service.DocumentCollectionService mockDocumentCollectionServiceForExtra(DocumentCollection draftCollection,
                                                                                                     DocumentCollection publishedCollection) {
        return (tech.easyflow.ai.service.DocumentCollectionService) Proxy.newProxyInstance(
                tech.easyflow.ai.service.DocumentCollectionService.class.getClassLoader(),
                new Class<?>[]{tech.easyflow.ai.service.DocumentCollectionService.class},
                (proxy, method, args) -> {
                    if ("getById".equals(method.getName())) {
                        return draftCollection;
                    }
                    if ("toPublishedView".equals(method.getName())) {
                        return publishedCollection;
                    }
                    if ("getPublishedById".equals(method.getName())) {
                        return publishedCollection;
                    }
                    return defaultValue(method.getReturnType());
                }
        );
    }

    private CategoryPermissionService mockCategoryPermissionService(RoleCategoryAccessSnapshot accessSnapshot) {
        return (CategoryPermissionService) Proxy.newProxyInstance(
                CategoryPermissionService.class.getClassLoader(),
                new Class<?>[]{CategoryPermissionService.class},
                (proxy, method, args) -> {
                    if ("getAccess".equals(method.getName())) {
                        return accessSnapshot;
                    }
                    if ("isSuperAdmin".equals(method.getName())) {
                        return accessSnapshot.isSuperAdmin();
                    }
                    return defaultValue(method.getReturnType());
                }
        );
    }

    private SysDeptService mockSysDeptService(Set<BigInteger> deptIds) {
        Set<BigInteger> readableDeptIds = deptIds == null ? Collections.emptySet() : deptIds;
        return (SysDeptService) Proxy.newProxyInstance(
                SysDeptService.class.getClassLoader(),
                new Class<?>[]{SysDeptService.class},
                (proxy, method, args) -> {
                    if ("getSelfAndAncestorDeptIds".equals(method.getName())) {
                        return readableDeptIds;
                    }
                    if ("canUserAccessDeptScopedResource".equals(method.getName())) {
                        BigInteger resourceDeptId = (BigInteger) args[1];
                        return readableDeptIds.contains(resourceDeptId);
                    }
                    return defaultValue(method.getReturnType());
                }
        );
    }

    private Set<BigInteger> setOf(BigInteger... values) {
        Set<BigInteger> result = new LinkedHashSet<>();
        Collections.addAll(result, values);
        return result;
    }

    private Object defaultValue(Class<?> returnType) {
        if (returnType == boolean.class) {
            return false;
        }
        if (returnType == int.class) {
            return 0;
        }
        if (returnType == long.class) {
            return 0L;
        }
        return null;
    }
}
