package tech.easyflow.ai.documentimport.task;

import com.easyagents.document.core.entity.DocumentBlock;
import com.easyagents.document.core.entity.DocumentImage;
import com.easyagents.document.core.entity.DocumentTable;
import com.easyagents.rag.ingestion.model.StrategyConfig;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.ai.document.model.DocumentParseArtifacts;
import tech.easyflow.ai.document.model.DocumentParsedResult;
import tech.easyflow.ai.documentimport.DocumentImportKeys;
import tech.easyflow.ai.entity.DocumentChunk;
import tech.easyflow.ai.entity.DocumentImportTask;
import tech.easyflow.ai.enums.DocumentImportTaskStatus;
import tech.easyflow.ai.enums.DocumentProcessStatus;
import tech.easyflow.ai.mapper.DocumentMapper;
import tech.easyflow.ai.service.DocumentImportTaskService;
import tech.easyflow.common.filestorage.FileStorageService;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

/**
 * {@link KnowledgeDocumentImportTaskAppService} 回归测试。
 *
 * @author Codex
 * @since 2026-04-15
 */
public class KnowledgeDocumentImportTaskAppServiceTest {

    /**
     * 验证向量化失败会按整文档失败语义重置进度，并刷新任务错误信息。
     *
     * @throws Exception 反射调用异常
     */
    @Test
    public void markIndexFailedShouldResetProgressAndPersistLatestError() throws Exception {
        BigInteger documentId = BigInteger.valueOf(10);
        BigInteger knowledgeId = BigInteger.valueOf(20);

        tech.easyflow.ai.entity.Document persistedDocument = new tech.easyflow.ai.entity.Document();
        persistedDocument.setId(documentId);
        persistedDocument.setCollectionId(knowledgeId);
        persistedDocument.setProcessStatus(DocumentProcessStatus.INDEXING.name());
        persistedDocument.setTotalChunks(8);
        persistedDocument.setCompletedChunks(5);
        persistedDocument.setFailedChunks(1);
        persistedDocument.setProgressPercent(63);
        persistedDocument.setLastTaskError("旧错误");

        AtomicReference<tech.easyflow.ai.entity.Document> updatedDocumentRef = new AtomicReference<tech.easyflow.ai.entity.Document>();
        AtomicReference<DocumentImportTask> updatedTaskRef = new AtomicReference<DocumentImportTask>();

        KnowledgeDocumentImportTaskAppService service = new KnowledgeDocumentImportTaskAppService();
        setField(service, "documentMapper", mockDocumentMapper(persistedDocument, updatedDocumentRef));
        setField(service, "documentImportTaskService", mockDocumentImportTaskService(updatedTaskRef));
        setField(service, "documentImportTaskStatusStreamService", new NoopTaskStatusStreamService());

        DocumentImportTask task = new DocumentImportTask();
        task.setId(BigInteger.valueOf(30));
        task.setDocumentId(documentId);
        task.setKnowledgeId(knowledgeId);
        task.setStatus(DocumentImportTaskStatus.RUNNING.name());
        task.setErrorSummary("旧错误");

        tech.easyflow.ai.entity.Document inputDocument = new tech.easyflow.ai.entity.Document();
        inputDocument.setId(documentId);
        inputDocument.setCollectionId(knowledgeId);

        Method method = KnowledgeDocumentImportTaskAppService.class.getDeclaredMethod(
            "markIndexFailed",
            DocumentImportTask.class,
            tech.easyflow.ai.entity.Document.class,
            String.class
        );
        method.setAccessible(true);
        method.invoke(service, task, inputDocument, "新错误");

        tech.easyflow.ai.entity.Document updatedDocument = updatedDocumentRef.get();
        Assert.assertNotNull(updatedDocument);
        Assert.assertEquals(DocumentProcessStatus.INDEX_FAILED.name(), updatedDocument.getProcessStatus());
        Assert.assertEquals(Integer.valueOf(0), updatedDocument.getCompletedChunks());
        Assert.assertEquals(Integer.valueOf(8), updatedDocument.getFailedChunks());
        Assert.assertEquals(Integer.valueOf(0), updatedDocument.getProgressPercent());
        Assert.assertEquals("新错误", updatedDocument.getLastTaskError());

        DocumentImportTask updatedTask = updatedTaskRef.get();
        Assert.assertNotNull(updatedTask);
        Assert.assertEquals(DocumentImportTaskStatus.FAILED.name(), updatedTask.getStatus());
        Assert.assertEquals("新错误", updatedTask.getErrorSummary());
    }

    /**
     * 验证知识库导入会把解析图片上传到对象存储，并同步改写 Markdown 与结构化引用。
     *
     * @throws Exception 反射调用异常
     */
    @Test
    public void normalizeParsedImagesForKnowledgeImportShouldUploadAndRewriteReferences() throws Exception {
        KnowledgeDocumentImportTaskAppService service = new KnowledgeDocumentImportTaskAppService();
        AtomicReference<String> savedPrePathRef = new AtomicReference<String>();
        AtomicReference<String> savedFilenameRef = new AtomicReference<String>();
        setField(service, "storageService", mockFileStorageService(savedPrePathRef, savedFilenameRef));

        tech.easyflow.ai.entity.Document document = new tech.easyflow.ai.entity.Document();
        document.setId(BigInteger.valueOf(88));
        document.setTitle("产品说明书（终版）.pdf");

        DocumentParsedResult parsedResult = new DocumentParsedResult();
        parsedResult.setMarkdown("图例如下：\n![](images/sample-image.png)");
        parsedResult.setPreferredText(parsedResult.getMarkdown());
        parsedResult.setPlainText(parsedResult.getMarkdown());

        DocumentImage image = new DocumentImage();
        image.setName("sample-image.png");
        image.setSourcePath("images/sample-image.png");
        image.setMimeType("image/png");
        image.setDataUrl("data:image/png;base64," + Base64.getEncoder().encodeToString("demo".getBytes(StandardCharsets.UTF_8)));
        parsedResult.setImages(new ArrayList<DocumentImage>(List.of(image)));

        DocumentBlock block = new DocumentBlock();
        block.setImagePath("images/sample-image.png");
        parsedResult.setBlocks(new ArrayList<DocumentBlock>(List.of(block)));

        DocumentTable table = new DocumentTable();
        table.setImagePath("images/sample-image.png");
        parsedResult.setTables(new ArrayList<DocumentTable>(List.of(table)));

        DocumentParseArtifacts artifacts = new DocumentParseArtifacts();
        List<Map<String, Object>> contentList = new ArrayList<Map<String, Object>>();
        Map<String, Object> contentItem = new LinkedHashMap<String, Object>();
        contentItem.put("img_path", "images/sample-image.png");
        contentList.add(contentItem);
        artifacts.setContentList(contentList);
        Map<String, Object> xlsxArtifact = new LinkedHashMap<String, Object>();
        List<Map<String, Object>> sheetImages = new ArrayList<Map<String, Object>>();
        sheetImages.add(new LinkedHashMap<String, Object>() {{
            put("sheetName", "Sheet1");
            put("sourcePaths", new ArrayList<String>(List.of("images/sample-image.png")));
        }});
        xlsxArtifact.put("sheetImages", sheetImages);
        artifacts.setExtraJsonArtifacts(new LinkedHashMap<String, Object>() {{
            put("xlsx", xlsxArtifact);
        }});
        parsedResult.setArtifacts(artifacts);

        Method method = KnowledgeDocumentImportTaskAppService.class.getDeclaredMethod(
            "normalizeParsedImagesForKnowledgeImport",
            tech.easyflow.ai.entity.Document.class,
            DocumentParsedResult.class
        );
        method.setAccessible(true);
        DocumentParsedResult normalized = (DocumentParsedResult) method.invoke(service, document, parsedResult);

        Assert.assertNotNull(normalized);
        Assert.assertEquals("knowledge-parse/88_产品说明书_终版/images", savedPrePathRef.get());
        Assert.assertEquals("sample-image.png", savedFilenameRef.get());

        String expectedUrl = "http://localhost:39000/easyflow/attachment/knowledge-parse/88_产品说明书_终版/images/sample-image.png";
        Assert.assertTrue(normalized.getMarkdown().contains(expectedUrl));
        Assert.assertEquals(expectedUrl, normalized.getBlocks().get(0).getImagePath());
        Assert.assertEquals(expectedUrl, normalized.getTables().get(0).getImagePath());
        Assert.assertEquals(expectedUrl, normalized.getImages().get(0).getSourcePath());
        Assert.assertNull(normalized.getImages().get(0).getDataUrl());
        Object rewrittenContentList = normalized.getArtifacts().getContentList();
        Assert.assertTrue(rewrittenContentList instanceof List<?>);
        Assert.assertEquals(expectedUrl, ((Map<?, ?>) ((List<?>) rewrittenContentList).get(0)).get("img_path"));
        Object rewrittenSheetImages = ((Map<?, ?>) normalized.getArtifacts().getExtraJsonArtifacts().get("xlsx")).get("sheetImages");
        Assert.assertTrue(rewrittenSheetImages instanceof List<?>);
        Object sourcePaths = ((Map<?, ?>) ((List<?>) rewrittenSheetImages).get(0)).get("sourcePaths");
        Assert.assertEquals(expectedUrl, ((List<?>) sourcePaths).get(0));
    }

    /**
     * 验证 PPTX 会基于页级工件生成稳定的知识库分块。
     *
     * @throws Exception 反射调用异常
     */
    @Test
    public void buildOfficeDocumentChunksShouldSplitPptxBySlide() throws Exception {
        KnowledgeDocumentImportTaskAppService service = new KnowledgeDocumentImportTaskAppService();
        tech.easyflow.ai.entity.Document document = new tech.easyflow.ai.entity.Document();
        document.setId(BigInteger.valueOf(101));
        document.setCollectionId(BigInteger.valueOf(201));
        document.setTitle("季度汇报.pptx");

        Map<String, Object> parseArtifactSummary = new LinkedHashMap<String, Object>();
        List<Map<String, Object>> slides = new ArrayList<Map<String, Object>>();
        slides.add(new LinkedHashMap<String, Object>() {{
            put("slideIndex", 0);
            put("title", "封面");
            put("ocrMarkdown", "本页介绍季度目标。");
            put("imagePath", "https://example.com/slides/slide-001.png");
            put("imageName", "slide-001-page");
        }});
        slides.add(new LinkedHashMap<String, Object>() {{
            put("slideIndex", 1);
            put("title", "经营分析");
            put("ocrMarkdown", "收入同比增长 18%。");
            put("imagePath", "https://example.com/slides/slide-002.png");
            put("imageName", "slide-002-page");
        }});
        parseArtifactSummary.put("slides", slides);

        Method method = KnowledgeDocumentImportTaskAppService.class.getDeclaredMethod(
            "buildOfficeDocumentChunks",
            tech.easyflow.ai.entity.Document.class,
            String.class,
            StrategyConfig.class,
            Map.class
        );
        method.setAccessible(true);

        @SuppressWarnings("unchecked")
        List<DocumentChunk> chunks = (List<DocumentChunk>) method.invoke(
            service,
            document,
            "pptx",
            null,
            parseArtifactSummary
        );

        Assert.assertEquals(2, chunks.size());
        DocumentChunk firstChunk = chunks.get(0);
        Assert.assertTrue(firstChunk.getContent().contains("Slide 1"));
        Assert.assertTrue(firstChunk.getContent().contains("本页介绍季度目标"));
        Assert.assertEquals("https://example.com/slides/slide-001.png",
            ((List<?>) firstChunk.getOptions().get(DocumentImportKeys.KEY_DOCUMENT_IMAGE_REFS)).get(0));
        Assert.assertEquals(1, firstChunk.getOptions().get(DocumentImportKeys.KEY_DOCUMENT_PAGE_INDEX));
        Assert.assertTrue(String.valueOf(firstChunk.getOptions().get(DocumentImportKeys.KEY_DOCUMENT_RENDER_MARKDOWN))
            .contains("slide-001.png"));
    }

    /**
     * 验证 XLSX 纯图片 Sheet 不会退化为空内容，并会输出稳定图片引用。
     *
     * @throws Exception 反射调用异常
     */
    @Test
    public void buildOfficeDocumentChunksShouldKeepImageOnlyXlsxSheetReferences() throws Exception {
        KnowledgeDocumentImportTaskAppService service = new KnowledgeDocumentImportTaskAppService();
        tech.easyflow.ai.entity.Document document = new tech.easyflow.ai.entity.Document();
        document.setId(BigInteger.valueOf(102));
        document.setCollectionId(BigInteger.valueOf(202));
        document.setTitle("巡检记录.xlsx");

        Map<String, Object> parseArtifactSummary = new LinkedHashMap<String, Object>();
        List<Map<String, Object>> sheets = new ArrayList<Map<String, Object>>();
        sheets.add(new LinkedHashMap<String, Object>() {{
            put("sheetName", "图片页");
            put("sheetIndex", 0);
            put("rows", new ArrayList<Map<String, Object>>());
        }});
        parseArtifactSummary.put("sheets", sheets);

        List<Map<String, Object>> cellImages = new ArrayList<Map<String, Object>>();
        cellImages.add(new LinkedHashMap<String, Object>() {{
            put("sheetName", "图片页");
            put("referenceKey", "image-sheet-r2c2-001");
            put("sourcePath", "https://example.com/xlsx/sheet/image-001.jpeg");
            put("anchorCell", "B2");
            put("ocrText", "设备状态正常");
            put("fromRow", 1);
        }});
        parseArtifactSummary.put("cellImages", cellImages);

        StrategyConfig strategyConfig = new StrategyConfig();
        strategyConfig.setRowsPerChunk(10);

        Method method = KnowledgeDocumentImportTaskAppService.class.getDeclaredMethod(
            "buildOfficeDocumentChunks",
            tech.easyflow.ai.entity.Document.class,
            String.class,
            StrategyConfig.class,
            Map.class
        );
        method.setAccessible(true);

        @SuppressWarnings("unchecked")
        List<DocumentChunk> chunks = (List<DocumentChunk>) method.invoke(
            service,
            document,
            "xlsx",
            strategyConfig,
            parseArtifactSummary
        );

        Assert.assertEquals(1, chunks.size());
        DocumentChunk onlyChunk = chunks.get(0);
        Assert.assertTrue(onlyChunk.getContent().contains("图片 OCR"));
        Assert.assertTrue(onlyChunk.getContent().contains("设备状态正常"));
        Assert.assertEquals("图片页", onlyChunk.getOptions().get(DocumentImportKeys.KEY_DOCUMENT_SHEET_NAME));
        Assert.assertEquals("https://example.com/xlsx/sheet/image-001.jpeg",
            ((List<?>) onlyChunk.getOptions().get(DocumentImportKeys.KEY_DOCUMENT_IMAGE_REFS)).get(0));
        String renderMarkdown = String.valueOf(onlyChunk.getOptions().get(DocumentImportKeys.KEY_DOCUMENT_RENDER_MARKDOWN));
        Assert.assertTrue(renderMarkdown.contains("[IMG:image-sheet-r2c2-001]"));
        Assert.assertTrue(renderMarkdown.contains("![image-sheet-r2c2-001](https://example.com/xlsx/sheet/image-001.jpeg)"));
    }

    /**
     * 验证空白 Sheet 不会被误判成纯图片分块。
     *
     * @throws Exception 反射调用异常
     */
    @Test
    public void buildOfficeDocumentChunksShouldSkipBlankXlsxSheetWithoutImages() throws Exception {
        KnowledgeDocumentImportTaskAppService service = new KnowledgeDocumentImportTaskAppService();
        tech.easyflow.ai.entity.Document document = new tech.easyflow.ai.entity.Document();
        document.setId(BigInteger.valueOf(103));
        document.setCollectionId(BigInteger.valueOf(203));
        document.setTitle("空白工作簿.xlsx");

        Map<String, Object> parseArtifactSummary = new LinkedHashMap<String, Object>();
        parseArtifactSummary.put("sheets", new ArrayList<Map<String, Object>>(List.of(new LinkedHashMap<String, Object>() {{
            put("sheetName", "空白页");
            put("sheetIndex", 0);
            put("rows", new ArrayList<Map<String, Object>>());
        }})));
        parseArtifactSummary.put("cellImages", new ArrayList<Map<String, Object>>());

        StrategyConfig strategyConfig = new StrategyConfig();
        strategyConfig.setRowsPerChunk(10);

        Method method = KnowledgeDocumentImportTaskAppService.class.getDeclaredMethod(
            "buildOfficeDocumentChunks",
            tech.easyflow.ai.entity.Document.class,
            String.class,
            StrategyConfig.class,
            Map.class
        );
        method.setAccessible(true);

        @SuppressWarnings("unchecked")
        List<DocumentChunk> chunks = (List<DocumentChunk>) method.invoke(
            service,
            document,
            "xlsx",
            strategyConfig,
            parseArtifactSummary
        );

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

    private static DocumentMapper mockDocumentMapper(tech.easyflow.ai.entity.Document persistedDocument,
                                                     AtomicReference<tech.easyflow.ai.entity.Document> updatedDocumentRef) {
        return (DocumentMapper) Proxy.newProxyInstance(
            DocumentMapper.class.getClassLoader(),
            new Class<?>[]{DocumentMapper.class},
            (proxy, method, args) -> {
                if ("selectOneById".equals(method.getName())) {
                    return persistedDocument;
                }
                if ("update".equals(method.getName())) {
                    updatedDocumentRef.set((tech.easyflow.ai.entity.Document) args[0]);
                    return 1;
                }
                return defaultValue(method.getReturnType());
            }
        );
    }

    private static DocumentImportTaskService mockDocumentImportTaskService(AtomicReference<DocumentImportTask> updatedTaskRef) {
        return (DocumentImportTaskService) Proxy.newProxyInstance(
            DocumentImportTaskService.class.getClassLoader(),
            new Class<?>[]{DocumentImportTaskService.class},
            (proxy, method, args) -> {
                if ("updateById".equals(method.getName())) {
                    updatedTaskRef.set((DocumentImportTask) args[0]);
                    return true;
                }
                return defaultValue(method.getReturnType());
            }
        );
    }

    private static FileStorageService mockFileStorageService(AtomicReference<String> savedPrePathRef,
                                                             AtomicReference<String> savedFilenameRef) {
        return (FileStorageService) Proxy.newProxyInstance(
            FileStorageService.class.getClassLoader(),
            new Class<?>[]{FileStorageService.class},
            (proxy, method, args) -> {
                if ("save".equals(method.getName()) && args != null && args.length == 2 && args[0] instanceof MultipartFile file) {
                    savedPrePathRef.set((String) args[1]);
                    savedFilenameRef.set(file.getOriginalFilename());
                    return "http://localhost:39000/easyflow/attachment/" + args[1] + "/" + file.getOriginalFilename();
                }
                return defaultValue(method.getReturnType());
            }
        );
    }

    private static void setField(Object target, String fieldName, Object value) throws Exception {
        Field field = KnowledgeDocumentImportTaskAppService.class.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(target, value);
    }

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

    /**
     * 测试用 SSE 推送桩，避免依赖线程池和真实推送。
     */
    private static class NoopTaskStatusStreamService extends DocumentImportTaskStatusStreamService {

        @Override
        public void publishAfterCommit(BigInteger documentId) {
            // no-op
        }
    }
}
