package tech.easyflow.ai.documentimport.task;

import com.easyagents.rag.core.BgeM3ChunkSafety;
import com.easyagents.rag.ingestion.model.StrategyConfig;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.documentimport.DocumentImportDtos;
import tech.easyflow.ai.entity.DocumentChunk;

import java.lang.reflect.Field;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * {@link CsvTableSnapshotService} 流式解析与分块测试。
 */
public class CsvTableSnapshotServiceTest {

    /**
     * 验证引号逗号、跨行字段、尾空列和重复表头都能稳定解析并按行窗口分块。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void shouldParseRfc4180AndBuildRowWindowChunks() throws Exception {
        InMemoryFileStorageService storage = new InMemoryFileStorageService();
        String sourcePath = "source/demo.csv";
        String csv = "姓名,备注,姓名,\r\n"
            + "张三,\"第一行\n第二行\",A,\r\n"
            + "李四,\"含,逗号\",B,\r\n";
        storage.put(sourcePath, csv.getBytes(StandardCharsets.UTF_8));
        CsvTableSnapshotService service = createService(storage);

        CsvTableSnapshotService.CsvParseResult result = service.parse(
            sourcePath, BigInteger.valueOf(7), BigInteger.valueOf(9), "token-1");

        Assert.assertEquals("UTF-8", result.getEncoding());
        Assert.assertEquals(2L, result.getRowCount());
        Assert.assertEquals(4, result.getColumnCount());
        Assert.assertEquals(
            List.of("姓名", "备注", "姓名_2", "列_4"), result.getHeaders());

        tech.easyflow.ai.entity.Document document =
            new tech.easyflow.ai.entity.Document();
        document.setId(BigInteger.valueOf(9));
        document.setCollectionId(BigInteger.valueOf(7));
        document.setTitle("demo.csv");
        document.setDocumentPath(sourcePath);
        StrategyConfig strategy = StrategyConfig.defaults();
        strategy.setStrategyCode("TABLE_ROW");
        strategy.setRowsPerChunk(1);

        DocumentImportDtos.PreviewSession session =
            service.buildChunkSnapshot(
                document, result.getManifestPath(), strategy);

        Assert.assertEquals(Integer.valueOf(2), session.getTotalChunks());
        Assert.assertEquals(2, session.getDocumentChunks().size());
        Assert.assertTrue(session.getDocumentChunks().get(0).getContent()
            .contains("第一行<br/>第二行"));
        Assert.assertTrue(session.getDocumentChunks().get(1).getContent()
            .contains("含,逗号"));
        Assert.assertTrue(session.getDocumentChunks().stream()
            .allMatch(chunk -> "TABLE_ROW".equals(
                chunk.getOptions().get("chunkType"))));
    }

    /**
     * 验证无 BOM 且非 UTF-8 的 CSV 会重新打开源文件并回退到 GB18030。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void shouldFallbackToGb18030AfterStrictUtf8Failure() throws Exception {
        InMemoryFileStorageService storage = new InMemoryFileStorageService();
        String sourcePath = "source/gb.csv";
        storage.put(
            sourcePath,
            "名称,说明\r\n测试,中文内容\r\n".getBytes(Charset.forName("GB18030")));
        CsvTableSnapshotService service = createService(storage);

        CsvTableSnapshotService.CsvParseResult result = service.parse(
            sourcePath, BigInteger.valueOf(7), BigInteger.valueOf(10), "token-2");

        Assert.assertEquals("GB18030", result.getEncoding());
        Assert.assertEquals(1L, result.getRowCount());
    }

    /**
     * 验证数据行列数与表头不一致时返回稳定失败码。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void shouldRejectColumnMismatchWithStableFailureCode() throws Exception {
        InMemoryFileStorageService storage = new InMemoryFileStorageService();
        String sourcePath = "source/broken.csv";
        storage.put(
            sourcePath,
            "a,b\r\n1,2,3\r\n".getBytes(StandardCharsets.UTF_8));
        CsvTableSnapshotService service = createService(storage);

        try {
            service.parse(
                sourcePath, BigInteger.valueOf(7), BigInteger.valueOf(11), "token-3");
            Assert.fail("应拒绝列数不一致的 CSV");
        } catch (CsvImportException error) {
            Assert.assertEquals(
                CsvTableSnapshotService.FAILURE_COLUMN_MISMATCH,
                error.getFailureCode());
            Assert.assertTrue(error.getMessage().contains("列数"));
        }
    }

    /**
     * 验证二进制伪装 CSV 中的 NUL 字节会被明确拒绝。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void shouldRejectNulByteAsMalformedCsv() throws Exception {
        InMemoryFileStorageService storage = new InMemoryFileStorageService();
        String sourcePath = "source/binary.csv";
        storage.put(
            sourcePath,
            "name,value\nalice,\0binary\n".getBytes(StandardCharsets.UTF_8));
        CsvTableSnapshotService service = createService(storage);

        try {
            service.parse(
                sourcePath, BigInteger.valueOf(7), BigInteger.valueOf(12), "token-4");
            Assert.fail("包含 NUL 字节的文件不应进入后续阶段");
        } catch (CsvImportException error) {
            Assert.assertEquals(
                CsvTableSnapshotService.FAILURE_MALFORMED,
                error.getFailureCode());
        }
    }

    /**
     * 验证超长表格正文会生成多个不超过数据库上限的续片。
     */
    @Test
    public void shouldSplitOversizedWindowWithoutSilentTruncation() {
        TabularRowWindowChunkBuilder builder =
            new TabularRowWindowChunkBuilder();
        String longValue = "长".repeat(
            TabularRowWindowChunkBuilder.MAX_CHUNK_CONTENT_CHARS + 100);

        List<DocumentChunk> chunks = builder.build(
            BigInteger.ONE,
            BigInteger.valueOf(2),
            "大表",
            List.of("正文"),
            List.of(new TabularRowWindowChunkBuilder.TabularRow(
                2, List.of(longValue))),
            1,
            "TABLE_ROW");

        Assert.assertTrue(chunks.size() > 1);
        Assert.assertTrue(chunks.stream().allMatch(
            chunk -> chunk.getContent().length()
                <= TabularRowWindowChunkBuilder.MAX_CHUNK_CONTENT_CHARS));
        Assert.assertTrue(chunks.stream().allMatch(
            chunk -> BgeM3ChunkSafety.isWithinHardLimit(chunk.getContent())));
        Assert.assertTrue(chunks.stream().allMatch(
            chunk -> chunk.getContent().startsWith("# 大表")
                && chunk.getContent().contains("| 正文 |")));
        Assert.assertTrue(chunks.stream().allMatch(
            chunk -> Integer.valueOf(2).equals(
                chunk.getOptions().get("rowStart"))
                && Integer.valueOf(2).equals(
                chunk.getOptions().get("rowEnd"))));
        long restoredChars = chunks.stream()
            .map(DocumentChunk::getContent)
            .flatMapToInt(String::chars)
            .filter(value -> value == '长')
            .count();
        Assert.assertEquals(longValue.length(), restoredChars);
        Assert.assertEquals(
            chunks.size(), chunks.get(0).getOptions().get("partTotal"));
    }

    /**
     * 验证超长记录优先按列边界续片，且每个续片都保留标题和列名。
     */
    @Test
    public void shouldSplitOversizedRowAtColumnBoundaries() {
        TabularRowWindowChunkBuilder builder =
            new TabularRowWindowChunkBuilder();
        String longValue = "长".repeat(20_000);

        List<DocumentChunk> chunks = builder.build(
            BigInteger.ONE,
            BigInteger.valueOf(2),
            "列边界表",
            List.of("左列", "正文", "右列"),
            List.of(new TabularRowWindowChunkBuilder.TabularRow(
                8, List.of("LEFT_MARKER", longValue, "RIGHT_MARKER"))),
            1,
            "TABLE_ROW");

        Assert.assertTrue(chunks.size() > 2);
        Assert.assertTrue(chunks.stream().allMatch(
            chunk -> chunk.getContent().startsWith("# 列边界表")
                && chunk.getContent().contains("\n| ")));
        String allContent = chunks.stream()
            .map(DocumentChunk::getContent)
            .reduce("", String::concat);
        Assert.assertEquals(1, occurrences(allContent, "LEFT_MARKER"));
        Assert.assertEquals(1, occurrences(allContent, "RIGHT_MARKER"));
        Assert.assertEquals(
            longValue.length(),
            allContent.chars().filter(value -> value == '长').count());
        Assert.assertTrue(chunks.stream().allMatch(
            chunk -> Integer.valueOf(8).equals(
                chunk.getOptions().get("rowStart"))
                && Integer.valueOf(8).equals(
                chunk.getOptions().get("rowEnd"))));
    }

    /**
     * 验证行数未满时仍会在加入下一行超过 Token 上限前闭合窗口。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void shouldCloseWindowBeforeNextRowExceedsTokenLimit()
        throws Exception {
        InMemoryFileStorageService storage =
            new InMemoryFileStorageService();
        String sourcePath = "source/token-window.csv";
        String value = "中".repeat(5_000);
        storage.put(
            sourcePath,
            ("正文\n" + value + "\n" + value + "\n")
                .getBytes(StandardCharsets.UTF_8));
        CsvTableSnapshotService service = createService(storage);
        CsvTableSnapshotService.CsvParseResult result = service.parse(
            sourcePath,
            BigInteger.valueOf(7),
            BigInteger.valueOf(13),
            "token-window");
        tech.easyflow.ai.entity.Document document =
            new tech.easyflow.ai.entity.Document();
        document.setId(BigInteger.valueOf(13));
        document.setCollectionId(BigInteger.valueOf(7));
        document.setTitle("token-window.csv");
        StrategyConfig strategy = StrategyConfig.defaults();
        strategy.setStrategyCode("TABLE_ROW");
        strategy.setRowsPerChunk(10);

        DocumentImportDtos.PreviewSession session =
            service.buildChunkSnapshot(
                document, result.getManifestPath(), strategy);

        Assert.assertEquals(Integer.valueOf(2), session.getTotalChunks());
        Assert.assertEquals(
            List.of(2, 3),
            session.getDocumentChunks().stream()
                .map(chunk -> (Integer) chunk.getOptions().get("rowStart"))
                .toList());
    }

    /**
     * 验证 CSV 行分片删除失败时清单保持可重试。
     *
     * @throws Exception 反射注入失败
     */
    @Test
    public void shouldKeepCsvManifestWhenPartDeletionFails()
        throws Exception {
        InMemoryFileStorageService storage =
            new InMemoryFileStorageService();
        String sourcePath = "source/cleanup.csv";
        storage.put(
            sourcePath,
            "a,b\n1,2\n".getBytes(StandardCharsets.UTF_8));
        CsvTableSnapshotService service = createService(storage);
        CsvTableSnapshotService.CsvParseResult result = service.parse(
            sourcePath,
            BigInteger.valueOf(7),
            BigInteger.valueOf(14),
            "cleanup");
        String manifestPath = result.getManifestPath();
        String partPath = storage.paths().stream()
            .filter(path -> !path.equals(sourcePath))
            .filter(path -> !path.equals(manifestPath))
            .findFirst()
            .orElseThrow();
        storage.failDelete(partPath, 1);

        try {
            service.delete(manifestPath);
            Assert.fail("CSV 行分片删除失败时应抛出异常");
        } catch (IllegalStateException expected) {
            Assert.assertTrue(expected.getMessage().contains("模拟删除失败"));
        }
        Assert.assertTrue(storage.contains(manifestPath));
        Assert.assertTrue(storage.contains(partPath));

        service.delete(manifestPath);
        Assert.assertEquals(1, storage.size());
        Assert.assertTrue(storage.contains(sourcePath));
    }

    /**
     * 统计文本出现次数。
     *
     * @param content 完整文本
     * @param target 目标文本
     * @return 出现次数
     */
    private int occurrences(String content, String target) {
        int count = 0;
        int offset = 0;
        while ((offset = content.indexOf(target, offset)) >= 0) {
            count++;
            offset += target.length();
        }
        return count;
    }

    /**
     * 创建完成依赖注入的被测服务。
     *
     * @param storage 内存文件存储
     * @return CSV 服务
     * @throws Exception 反射注入失败
     */
    private CsvTableSnapshotService createService(
        InMemoryFileStorageService storage) throws Exception {
        DocumentImportChunkSnapshotService chunkSnapshotService =
            new DocumentImportChunkSnapshotService();
        setField(chunkSnapshotService, "storageService", storage);

        CsvTableSnapshotService service = new CsvTableSnapshotService();
        setField(service, "storageService", storage);
        setField(service, "chunkSnapshotService", chunkSnapshotService);
        return service;
    }

    /**
     * 反射设置测试字段。
     *
     * @param target 目标对象
     * @param fieldName 字段名
     * @param value 字段值
     * @throws Exception 字段不存在或不可访问
     */
    private void setField(
        Object target,
        String fieldName,
        Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(target, value);
    }
}
