package tech.easyflow.agent.runtime.document;

import com.mybatisflex.core.query.QueryWrapper;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import tech.easyflow.agent.config.AgentDocumentProperties;
import tech.easyflow.agent.entity.AgentDocumentAttachment;
import tech.easyflow.agent.mapper.AgentDocumentAttachmentMapper;
import tech.easyflow.agent.mapper.AgentDocumentSnapshotMapper;
import tech.easyflow.agent.runtime.media.AgentMediaObjectStorage;
import tech.easyflow.common.entity.LoginAccount;

import java.io.ByteArrayInputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * {@link AgentDocumentService} 上传幂等性测试。
 */
public class AgentDocumentServiceTest {

    private static final byte[] OLE_SIGNATURE = {
            (byte) 0xd0, (byte) 0xcf, 0x11, (byte) 0xe0, (byte) 0xa1, (byte) 0xb1, 0x1a, (byte) 0xe1
    };
    private static final byte[] ZIP_SIGNATURE = {'P', 'K', 3, 4, 0, 0, 0, 0};

    private AgentDocumentAttachmentMapper attachmentMapper;
    private AgentMediaObjectStorage objectStorage;
    private AgentDocumentReadTaskProducer taskProducer;
    private AgentDocumentService service;
    private LoginAccount account;

    /**
     * 初始化文档服务测试依赖。
     */
    @Before
    public void setUp() {
        attachmentMapper = Mockito.mock(AgentDocumentAttachmentMapper.class);
        objectStorage = Mockito.mock(AgentMediaObjectStorage.class);
        taskProducer = Mockito.mock(AgentDocumentReadTaskProducer.class);
        service = new AgentDocumentService(
                attachmentMapper,
                Mockito.mock(AgentDocumentSnapshotMapper.class),
                objectStorage,
                new AgentDocumentProperties(),
                taskProducer);
        account = new LoginAccount();
        account.setTenantId(BigInteger.valueOf(2));
        account.setId(BigInteger.valueOf(7));
    }

    /**
     * 验证相同上传 ID 重试时复用现有附件，不重复写入对象存储。
     *
     * @throws Exception 模拟上传流读取失败
     */
    @Test
    public void uploadShouldReuseExistingAttachment() throws Exception {
        String uploadId = "0123456789abcdef0123456789abcdef";
        AgentDocumentAttachment existing = existingAttachment(uploadId);
        Mockito.when(attachmentMapper.selectOneByQuery(Mockito.any(QueryWrapper.class)))
                .thenReturn(existing);

        MultipartFile file = Mockito.mock(MultipartFile.class);
        Mockito.when(file.isEmpty()).thenReturn(false);
        Mockito.when(file.getSize()).thenReturn(5L);
        Mockito.when(file.getOriginalFilename()).thenReturn("sample.txt");
        Mockito.when(file.getContentType()).thenReturn("text/plain");
        Mockito.doAnswer(invocation -> new ByteArrayInputStream("hello".getBytes()))
                .when(file).getInputStream();

        AgentDocumentUploadView result = service.upload(
                file,
                AgentDocumentService.MODE_FORMAL,
                "9",
                "session-1",
                uploadId,
                account);

        Assert.assertEquals(uploadId, result.getUploadId());
        Assert.assertEquals("READY", result.getStatus());
        Mockito.verify(attachmentMapper, Mockito.never()).insert(Mockito.any());
        Mockito.verify(objectStorage, Mockito.never()).put(
                Mockito.anyString(), Mockito.any(), Mockito.anyLong(), Mockito.anyString());
        Mockito.verify(taskProducer, Mockito.never()).send(Mockito.anyString());
    }

    /**
     * 验证 WPS、系统注册表和旧式浏览器 MIME 别名在内容有效时均可上传。
     *
     * @throws Exception 反射调用失败时抛出
     */
    @Test
    public void validateFileShouldAcceptClientMimeAliasesAndNormalizeCanonicalType() throws Exception {
        List<MimeCase> cases = List.of(
                new MimeCase("sample.doc", "application/wps-office.doc",
                        OLE_SIGNATURE, "application/msword"),
                new MimeCase("sample.docx", "application/wps-office.docx",
                        ZIP_SIGNATURE, "application/vnd.openxmlformats-officedocument.wordprocessingml.document"),
                new MimeCase("sample.xls", "application/wps-office.xls",
                        OLE_SIGNATURE, "application/vnd.ms-excel"),
                new MimeCase("sample.xlsx", "application/x-zip-compressed",
                        ZIP_SIGNATURE, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
                new MimeCase("sample.ppt", "application/x-ole-storage",
                        OLE_SIGNATURE, "application/vnd.ms-powerpoint"),
                new MimeCase("sample.pptx", "application/zip",
                        ZIP_SIGNATURE, "application/vnd.openxmlformats-officedocument.presentationml.presentation"),
                new MimeCase("sample.pdf", "application/x-pdf",
                        "%PDF-1.7".getBytes(StandardCharsets.US_ASCII), "application/pdf"),
                new MimeCase("sample.txt", "text/x-log",
                        "plain text".getBytes(StandardCharsets.UTF_8), "text/plain"),
                new MimeCase("sample.md", "text/x-markdown",
                        "# title".getBytes(StandardCharsets.UTF_8), "text/markdown"));

        for (MimeCase mimeCase : cases) {
            Object document = invokeValidateFile(file(mimeCase));
            Assert.assertEquals(mimeCase.canonicalMime(), mimeType(document));
        }
    }

    /**
     * 验证客户端 MIME 即使伪装成 WPS 文档，文件头不匹配时仍会被拒绝。
     *
     * @throws Exception 反射调用失败时抛出
     */
    @Test
    public void validateFileShouldRejectInvalidContentDespiteCompatibleMimeAlias() throws Exception {
        MimeCase mimeCase = new MimeCase(
                "sample.doc",
                "application/wps-office.doc",
                "%PDF-1.7".getBytes(StandardCharsets.US_ASCII),
                "application/msword");

        try {
            invokeValidateFile(file(mimeCase));
            Assert.fail("expected ResponseStatusException");
        } catch (ResponseStatusException error) {
            Assert.assertEquals(415, error.getStatusCode().value());
            Assert.assertEquals("文件内容与扩展名不一致", error.getReason());
        }
    }

    /**
     * 构造一条已完成读取的正式聊天附件。
     *
     * @param uploadId 上传 ID
     * @return 附件记录
     */
    private AgentDocumentAttachment existingAttachment(String uploadId) {
        AgentDocumentAttachment attachment = new AgentDocumentAttachment();
        attachment.setAttachmentId("abcdef0123456789abcdef0123456789");
        attachment.setUploadId(uploadId);
        attachment.setTenantId(BigInteger.valueOf(2));
        attachment.setUserId(BigInteger.valueOf(7));
        attachment.setAgentId(BigInteger.valueOf(9));
        attachment.setMode(AgentDocumentService.MODE_FORMAL);
        attachment.setSessionId("session-1");
        attachment.setOriginalName("sample.txt");
        attachment.setExtension("txt");
        attachment.setMimeType("text/plain");
        attachment.setFileSize(5L);
        attachment.setStatus(AgentDocumentStatus.READY.name());
        return attachment;
    }

    /**
     * 创建指定名称、MIME 和内容的上传文件。
     *
     * @param mimeCase MIME 测试用例
     * @return 上传文件
     * @throws Exception 模拟输入流失败时抛出
     */
    private MultipartFile file(MimeCase mimeCase) throws Exception {
        MultipartFile file = Mockito.mock(MultipartFile.class);
        Mockito.when(file.isEmpty()).thenReturn(false);
        Mockito.when(file.getSize()).thenReturn((long) mimeCase.bytes().length);
        Mockito.when(file.getOriginalFilename()).thenReturn(mimeCase.fileName());
        Mockito.when(file.getContentType()).thenReturn(mimeCase.declaredMime());
        Mockito.when(file.getInputStream())
                .thenAnswer(invocation -> new ByteArrayInputStream(mimeCase.bytes()));
        return file;
    }

    /**
     * 调用文档文件校验。
     *
     * @param file 上传文件
     * @return 内部文档描述
     * @throws Exception 校验失败或反射调用失败时抛出
     */
    private Object invokeValidateFile(MultipartFile file) throws Exception {
        Method method = AgentDocumentService.class.getDeclaredMethod("validateFile", MultipartFile.class);
        method.setAccessible(true);
        try {
            return method.invoke(service, file);
        } catch (InvocationTargetException error) {
            if (error.getCause() instanceof Exception cause) {
                throw cause;
            }
            throw error;
        }
    }

    /**
     * 读取内部文档描述的规范 MIME。
     *
     * @param document 内部文档描述
     * @return 规范 MIME
     * @throws Exception 反射调用失败时抛出
     */
    private String mimeType(Object document) throws Exception {
        Method method = document.getClass().getDeclaredMethod("mimeType");
        method.setAccessible(true);
        return (String) method.invoke(document);
    }

    /**
     * MIME 兼容性测试用例。
     *
     * @param fileName 文件名
     * @param declaredMime 客户端声明 MIME
     * @param bytes 文件内容
     * @param canonicalMime 服务端规范 MIME
     */
    private record MimeCase(String fileName,
                            String declaredMime,
                            byte[] bytes,
                            String canonicalMime) {
    }
}
