package tech.easyflow.ai.easyagents.tool;

import com.easyagents.core.document.Document;
import com.easyagents.rag.retrieval.RetrievalMode;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
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.entity.DocumentCollection;
import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper;
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
import tech.easyflow.ai.service.DocumentCollectionService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.util.SpringContextUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
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.Proxy;
import java.math.BigInteger;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.FutureTask;

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

    /**
     * 直接构造一个本不应暴露的知识库 Tool 调用时，必须抛出无权限异常。
     *
     * @throws Exception 反射注入异常
     */
    @Test
    public void invokeShouldThrowWhenChatTimeKnowledgeUnavailable() throws Exception {
        TestDocumentCollectionService documentCollectionService = new TestDocumentCollectionService(
                buildKnowledge(101, 11, 21, null, "PUBLIC"),
                List.of(buildSearchDocument("should-not-reach"))
        );
        ChatTimeToolAvailabilityService availabilityService = buildAvailabilityService(
                new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(99))),
                Collections.emptySet()
        );
        ApplicationContext previousContext = getStaticField("applicationContext");
        Object previousBeanFactory = getStaticField("beanFactory");
        try {
            setStaticField("beanFactory", null);
            setStaticField("applicationContext", mockApplicationContext(documentCollectionService.toProxy(), availabilityService));
            DocumentCollectionTool tool = new DocumentCollectionTool(
                    buildKnowledge(101, 11, 21, null, "PUBLIC"),
                    false,
                    RetrievalMode.HYBRID,
                    buildContext(12, 3)
            );

            BusinessException exception = Assert.assertThrows(
                    BusinessException.class,
                    () -> tool.invoke(Map.of("input", "test"))
            );

            Assert.assertEquals("当前用户无权在聊天中访问该知识库", exception.getMessage());
            Assert.assertEquals(0, documentCollectionService.searchCount);
        } finally {
            setStaticField("applicationContext", previousContext);
            setStaticField("beanFactory", previousBeanFactory);
        }
    }

    /**
     * 异步线程中即使没有当前线程登录态，也应能基于显式快照完成判权并执行检索。
     *
     * @throws Exception 反射注入异常
     */
    @Test
    public void invokeShouldUseExplicitLoginSnapshotWithoutThreadState() throws Exception {
        TestDocumentCollectionService documentCollectionService = new TestDocumentCollectionService(
                buildKnowledge(101, 11, 21, null, "PUBLIC"),
                List.of(buildSearchDocument("知识片段A"), buildSearchDocument("知识片段B"))
        );
        ChatTimeToolAvailabilityService availabilityService = buildAvailabilityService(
                new RoleCategoryAccessSnapshot("KNOWLEDGE", BigInteger.valueOf(12), false, false, setOf(BigInteger.valueOf(21))),
                Collections.emptySet()
        );
        ApplicationContext previousContext = getStaticField("applicationContext");
        Object previousBeanFactory = getStaticField("beanFactory");
        try {
            setStaticField("beanFactory", null);
            setStaticField("applicationContext", mockApplicationContext(documentCollectionService.toProxy(), availabilityService));
            DocumentCollectionTool tool = new DocumentCollectionTool(
                    buildKnowledge(101, 11, 21, null, "PUBLIC"),
                    false,
                    RetrievalMode.KEYWORD,
                    buildContext(12, 3)
            );
            FutureTask<Object> task = new FutureTask<>(() -> tool.invoke(Map.of("input", "异步查询")));
            Thread thread = new Thread(task, "document-collection-tool-test");
            thread.start();

            Object result = task.get();

            Assert.assertEquals("知识片段A\n\n---\n\n知识片段B", result);
            Assert.assertEquals(1, documentCollectionService.searchCount);
            Assert.assertNotNull(documentCollectionService.lastRequest);
            Assert.assertEquals("异步查询", documentCollectionService.lastRequest.getQuery());
            Assert.assertEquals(RetrievalMode.KEYWORD, documentCollectionService.lastRequest.getRetrievalMode());
            Assert.assertEquals("BOT_TOOL", documentCollectionService.lastRequest.getCallerType());
            Assert.assertEquals("101", documentCollectionService.lastRequest.getCallerId());
        } finally {
            setStaticField("applicationContext", previousContext);
            setStaticField("beanFactory", previousBeanFactory);
        }
    }

    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 DocumentCollection buildKnowledge(long knowledgeId,
                                              long createdBy,
                                              long categoryId,
                                              BigInteger deptId,
                                              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.setDeptId(deptId);
        knowledge.setVisibilityScope(visibilityScope);
        return knowledge;
    }

    private Document buildSearchDocument(String content) {
        Document document = new Document();
        document.setContent(content);
        return document;
    }

    private ApplicationContext mockApplicationContext(DocumentCollectionService documentCollectionService,
                                                      ChatTimeToolAvailabilityService availabilityService) {
        return (ApplicationContext) Proxy.newProxyInstance(
                ApplicationContext.class.getClassLoader(),
                new Class[]{ApplicationContext.class},
                (proxy, method, args) -> {
                    if ("getBean".equals(method.getName()) && args != null && args.length == 1 && args[0] instanceof Class<?> clazz) {
                        if (clazz == DocumentCollectionService.class) {
                            return documentCollectionService;
                        }
                        if (clazz == ChatTimeToolAvailabilityService.class) {
                            return availabilityService;
                        }
                    }
                    if ("equals".equals(method.getName())) {
                        return proxy == args[0];
                    }
                    if ("hashCode".equals(method.getName())) {
                        return System.identityHashCode(proxy);
                    }
                    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());
                }
        );
    }

    @SuppressWarnings("unchecked")
    private <T> T getStaticField(String fieldName) throws Exception {
        Field field = SpringContextUtil.class.getDeclaredField(fieldName);
        field.setAccessible(true);
        return (T) field.get(null);
    }

    private void setStaticField(String fieldName, Object value) throws Exception {
        Field field = SpringContextUtil.class.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(null, value);
    }

    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;
    }

    /**
     * 记录检索调用的最小知识库服务桩。
     */
    private static class TestDocumentCollectionService {

        private final DocumentCollection knowledge;
        private final List<Document> searchResult;
        private int searchCount;
        private KnowledgeRetrievalRequest lastRequest;

        private TestDocumentCollectionService(DocumentCollection knowledge, List<Document> searchResult) {
            this.knowledge = knowledge;
            this.searchResult = searchResult;
        }

        private DocumentCollectionService toProxy() {
            return (DocumentCollectionService) Proxy.newProxyInstance(
                    DocumentCollectionService.class.getClassLoader(),
                    new Class<?>[]{DocumentCollectionService.class},
                    (proxy, method, args) -> {
                        if ("getById".equals(method.getName())) {
                            return knowledge;
                        }
                        if ("search".equals(method.getName()) && args != null && args.length == 1 && args[0] instanceof KnowledgeRetrievalRequest request) {
                            this.searchCount++;
                            this.lastRequest = request;
                            return searchResult;
                        }
                        return defaultStaticValue(method.getReturnType());
                    }
            );
        }

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