package tech.easyflow.ai.node;

import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.document.model.DocumentParseTaskInfo;
import tech.easyflow.ai.document.model.DocumentParseTaskStatus;
import tech.easyflow.ai.document.model.DocumentParsedResult;
import tech.easyflow.ai.document.model.DocumentSourceRef;
import tech.easyflow.ai.document.service.DocumentParseBridgeService;
import tech.easyflow.common.filestorage.FileStorageService;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.sun.net.httpserver.HttpServer;

/**
 * {@link DocNodeFileContentExtractor} 单元测试。
 *
 * @author Codex
 * @since 2026-04-14
 */
public class DocNodeFileContentExtractorTest {

    /**
     * 验证 PDF 文件会走统一文档解析桥接服务。
     */
    @Test
    public void shouldUseDocumentBridgeForPdf() {
        RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
        DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
            bridgeService,
            new FakeFileStorageService(),
            new FakeReaderManager("ignored")
        );

        String content = extractor.extract(buildFileValue("demo.pdf", "/files/demo.pdf", "application/pdf"));

        Assert.assertEquals("# parsed", content);
        Assert.assertNotNull(bridgeService.lastSource);
        Assert.assertEquals("demo.pdf", bridgeService.lastSource.getFileName());
    }

    /**
     * 验证 DOCX 文件会走统一文档解析桥接服务。
     */
    @Test
    public void shouldUseDocumentBridgeForDocx() {
        RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
        DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
            bridgeService,
            new FakeFileStorageService(),
            new FakeReaderManager("ignored")
        );

        String content = extractor.extract(buildFileValue("demo.docx", "/files/demo.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"));

        Assert.assertEquals("# parsed", content);
        Assert.assertNotNull(bridgeService.lastSource);
        Assert.assertEquals("demo.docx", bridgeService.lastSource.getFileName());
    }

    /**
     * 验证 PPTX 文件会走统一文档解析桥接服务。
     */
    @Test
    public void shouldUseDocumentBridgeForPptx() {
        RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
        DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
            bridgeService,
            new FakeFileStorageService(),
            new FakeReaderManager("ignored")
        );

        String content = extractor.extract(buildFileValue(
            "slides.pptx",
            "/files/slides.pptx",
            "application/vnd.openxmlformats-officedocument.presentationml.presentation"
        ));

        Assert.assertEquals("# parsed", content);
        Assert.assertNotNull(bridgeService.lastSource);
        Assert.assertEquals("slides.pptx", bridgeService.lastSource.getFileName());
    }

    /**
     * 验证 XLSX 文件会走统一文档解析桥接服务。
     */
    @Test
    public void shouldUseDocumentBridgeForXlsx() {
        RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
        DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
            bridgeService,
            new FakeFileStorageService(),
            new FakeReaderManager("ignored")
        );

        String content = extractor.extract(buildFileValue(
            "table.xlsx",
            "/files/table.xlsx",
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        ));

        Assert.assertEquals("# parsed", content);
        Assert.assertNotNull(bridgeService.lastSource);
        Assert.assertEquals("table.xlsx", bridgeService.lastSource.getFileName());
    }

    /**
     * 验证非桥接类型文件会继续走默认读取器。
     */
    @Test
    public void shouldUseDefaultReaderForUnsupportedType() {
        RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
        DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
            bridgeService,
            new FakeFileStorageService(),
            new FakeReaderManager("plain text")
        );

        String content = extractor.extract(buildFileValue("note.txt", "/files/note.txt", "text/plain"));

        Assert.assertEquals("plain text", content);
        Assert.assertNull(bridgeService.lastSource);
    }

    /**
     * 验证缺少 filePath 时会抛出明确异常。
     */
    @Test
    public void shouldRejectMissingFilePath() {
        DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
            new RecordingDocumentParseBridgeService(),
            new FakeFileStorageService(),
            new FakeReaderManager("plain text")
        );

        try {
            extractor.extract(buildFileValue("demo.pdf", null, "application/pdf"));
            Assert.fail("expected BusinessException");
        } catch (BusinessException e) {
            Assert.assertEquals("文件输入缺少 filePath", e.getMessage());
        }
    }

    /**
     * 验证桥接解析结果为空时不会回退旧读取链路。
     */
    @Test
    public void shouldFailWhenBridgeParseResultIsEmpty() {
        RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
        bridgeService.response.setPreferredText(null);
        bridgeService.response.setMarkdown(null);
        bridgeService.response.setPlainText(null);
        DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
            bridgeService,
            new FakeFileStorageService(),
            new FakeReaderManager("pdf fallback")
        );

        try {
            extractor.extract(buildFileValue("demo.pdf", "/files/demo.pdf", "application/pdf"));
            Assert.fail("expected BusinessException");
        } catch (BusinessException e) {
            Assert.assertEquals("文档解析结果为空", e.getMessage());
        }
    }

    /**
     * 验证远端素材 URL 的非桥接文件不会误走本地存储读取。
     */
    @Test
    public void shouldReadRemoteUrlForUnsupportedType() {
        RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
        HttpServer server;
        try {
            server = HttpServer.create(new InetSocketAddress(0), 0);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        byte[] body = "remote text".getBytes(StandardCharsets.UTF_8);
        server.createContext("/note.txt", exchange -> {
            exchange.sendResponseHeaders(200, body.length);
            exchange.getResponseBody().write(body);
            exchange.close();
        });
        server.start();
        try {
            DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
                bridgeService,
                new FailingFileStorageService(),
                new ReadingReaderManager()
            );

            String content = extractor.extract(buildFileValue(
                "note.txt",
                "http://127.0.0.1:" + server.getAddress().getPort() + "/note.txt",
                "text/plain"
            ));

            Assert.assertEquals("remote text", content);
            Assert.assertNull(bridgeService.lastSource);
        } finally {
            server.stop(0);
        }
    }

    /**
     * 验证多文件输入会按顺序返回逐文件结果。
     */
    @Test
    public void shouldExtractDocumentsForMultipleFiles() {
        RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
        DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
            bridgeService,
            new FakeFileStorageService(),
            new FakeReaderManager("plain text")
        );

        List<DocNodeFileContentExtractor.DocExtractedDocument> documents = extractor.extractDocuments(Arrays.asList(
            buildFileValue("demo.pdf", "/files/demo.pdf", "application/pdf"),
            buildFileValue("note.txt", "/files/note.txt", "text/plain")
        ));

        Assert.assertEquals(2, documents.size());
        Assert.assertEquals("demo.pdf", documents.get(0).getFileName());
        Assert.assertEquals("# parsed", documents.get(0).getContent());
        Assert.assertEquals("note.txt", documents.get(1).getFileName());
        Assert.assertEquals("plain text", documents.get(1).getContent());
    }

    /**
     * 验证多文件中任一文件失败时会暴露文件名。
     */
    @Test
    public void shouldExposeFileNameWhenMultipleDocumentsFail() {
        RecordingDocumentParseBridgeService bridgeService = new RecordingDocumentParseBridgeService();
        bridgeService.response.setPreferredText(null);
        bridgeService.response.setMarkdown(null);
        bridgeService.response.setPlainText(null);
        DocNodeFileContentExtractor extractor = new DocNodeFileContentExtractor(
            bridgeService,
            new FakeFileStorageService(),
            new FakeReaderManager("plain text")
        );

        try {
            extractor.extractDocuments(Arrays.asList(
                buildFileValue("broken.pdf", "/files/broken.pdf", "application/pdf"),
                buildFileValue("note.txt", "/files/note.txt", "text/plain")
            ));
            Assert.fail("expected BusinessException");
        } catch (BusinessException e) {
            Assert.assertEquals("文件解析失败(broken.pdf): 文档解析结果为空", e.getMessage());
        }
    }

    private Map<String, Object> buildFileValue(String fileName, String filePath, String contentType) {
        Map<String, Object> value = new HashMap<String, Object>();
        value.put("fileName", fileName);
        value.put("filePath", filePath);
        value.put("contentType", contentType);
        value.put("size", 16L);
        value.put("url", filePath);
        return value;
    }

    private static class RecordingDocumentParseBridgeService implements DocumentParseBridgeService {

        private final DocumentParsedResult response = new DocumentParsedResult();
        private DocumentSourceRef lastSource;

        private RecordingDocumentParseBridgeService() {
            response.setPreferredText("# parsed");
            response.setMarkdown("# parsed");
            response.setPlainText("parsed");
        }

        @Override
        public DocumentParsedResult parse(DocumentSourceRef source, tech.easyflow.ai.document.model.DocumentParseScenario scenario) {
            this.lastSource = source;
            return response;
        }

        @Override
        public DocumentParseTaskStatus submit(DocumentSourceRef source, tech.easyflow.ai.document.model.DocumentParseScenario scenario) {
            return new DocumentParseTaskStatus();
        }

        @Override
        public DocumentParseTaskStatus queryTask(String taskId) {
            return new DocumentParseTaskStatus();
        }

        @Override
        public DocumentParsedResult queryResult(String taskId) {
            return response;
        }

        @Override
        public DocumentParseTaskInfo queryTaskInfo(String taskId) {
            return new DocumentParseTaskInfo();
        }
    }

    private static class FakeFileStorageService implements FileStorageService {

        @Override
        public String save(org.springframework.web.multipart.MultipartFile file) {
            return null;
        }

        @Override
        public void delete(String path) {
        }

        @Override
        public String save(File file, String prePath) {
            return null;
        }

        @Override
        public InputStream readStream(String path) throws IOException {
            return new ByteArrayInputStream("doc".getBytes(StandardCharsets.UTF_8));
        }

        @Override
        public long getFileSize(String path) {
            return 3L;
        }
    }

    private static class FakeReaderManager extends ReaderManager {

        private final String content;

        private FakeReaderManager(String content) {
            this.content = content;
        }

        @Override
        public ReadDocService getReader() {
            return (fileName, is) -> content;
        }
    }

    private static class ReadingReaderManager extends ReaderManager {

        @Override
        public ReadDocService getReader() {
            return (fileName, is) -> {
                try {
                    return new String(is.readAllBytes(), StandardCharsets.UTF_8);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            };
        }
    }

    private static class FailingFileStorageService implements FileStorageService {

        @Override
        public String save(org.springframework.web.multipart.MultipartFile file) {
            return null;
        }

        @Override
        public void delete(String path) {
        }

        @Override
        public String save(File file, String prePath) {
            return null;
        }

        @Override
        public InputStream readStream(String path) throws IOException {
            throw new IOException("should not read remote url from storage");
        }

        @Override
        public long getFileSize(String path) {
            return 0L;
        }
    }
}
