package tech.easyflow.ai.documentimport.task;

import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.ai.documentimport.DocumentImportBatchCreateContext;
import tech.easyflow.ai.documentimport.DocumentImportBatchDtos;
import tech.easyflow.ai.documentimport.ImportCallerContext;
import tech.easyflow.ai.documentimport.ImportCallerType;
import tech.easyflow.ai.documentimport.PublicDocumentImportDtos;
import tech.easyflow.ai.entity.DocumentImportBatch;
import tech.easyflow.ai.entity.DocumentImportBatchItem;
import tech.easyflow.ai.enums.DocumentImportBatchItemStage;
import tech.easyflow.ai.enums.DocumentImportBatchItemStatus;
import tech.easyflow.ai.enums.DocumentImportBatchStatus;
import tech.easyflow.ai.mapper.DocumentImportBatchMapper;
import tech.easyflow.ai.service.DocumentImportBatchItemService;
import tech.easyflow.ai.service.DocumentImportBatchService;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.io.ByteArrayInputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Public API 批量导入门面边界与服务端去重测试。
 *
 * @author Codex
 * @since 2026-08-02
 */
public class KnowledgeImportBatchFacadeTest {

    /**
     * 验证单次文件数超过 200 时在创建批次前拒绝。
     */
    @Test
    public void submitShouldRejectMoreThanTwoHundredFiles() {
        TestContext context = createContext();
        PublicDocumentImportDtos.BatchMetadata metadata =
            new PublicDocumentImportDtos.BatchMetadata();
        metadata.setKnowledgeId(BigInteger.ONE);
        List<DocumentImportBatchDtos.ManifestItem> manifest = new ArrayList<>();
        List<MultipartFile> files = new ArrayList<>();
        for (int index = 0; index < 201; index++) {
            String name = "file-" + index + ".txt";
            manifest.add(manifest(name, name, 1L));
            files.add(file(name, new byte[]{'a'}));
        }
        metadata.setFiles(manifest);

        try {
            context.facade.submit(context.caller, metadata, files);
            Assert.fail("Expected file count rejection");
        } catch (BusinessException expected) {
            Assert.assertTrue(expected.getMessage().contains("最多上传200个文件"));
        }
        Mockito.verifyNoInteractions(context.batchAppService);
    }

    /**
     * 验证文件夹清单拒绝父目录穿越。
     */
    @Test
    public void submitShouldRejectRelativePathTraversal() {
        TestContext context = createContext();
        PublicDocumentImportDtos.BatchMetadata metadata =
            metadata("demo.txt", "../demo.txt", 4L);

        try {
            context.facade.submit(
                context.caller,
                metadata,
                List.of(file(
                    "demo.txt",
                    "demo".getBytes(StandardCharsets.UTF_8)
                ))
            );
            Assert.fail("Expected relative path rejection");
        } catch (BusinessException expected) {
            Assert.assertTrue(expected.getMessage().contains("相对路径无效"));
        }
        Mockito.verifyNoInteractions(context.batchAppService);
    }

    /**
     * 验证 Public API 可省略 fileSize，内部批次使用服务端实测大小。
     */
    @Test
    public void submitShouldUseMeasuredSizeWhenMetadataOmitsFileSize() {
        TestContext context = createContext();
        PublicDocumentImportDtos.BatchMetadata metadata =
            metadata("demo.txt", "folder/demo.txt", 4L);
        metadata.getFiles().get(0).setFileSize(null);
        byte[] content = "demo".getBytes(StandardCharsets.UTF_8);

        DocumentImportBatchDtos.CreateResponse created =
            new DocumentImportBatchDtos.CreateResponse();
        created.setBatchId(BigInteger.valueOf(91));
        DocumentImportBatchDtos.ItemResponse createdItem =
            new DocumentImportBatchDtos.ItemResponse();
        createdItem.setItemId(BigInteger.valueOf(92));
        created.setItems(List.of(createdItem));
        Mockito.when(context.batchAppService.createBatch(
            Mockito.any(), Mockito.any()
        )).thenReturn(created);
        Mockito.when(context.itemService.updateById(Mockito.any()))
            .thenReturn(true);
        DocumentImportBatch batch = new DocumentImportBatch();
        batch.setId(created.getBatchId());
        batch.setKnowledgeId(BigInteger.ONE);
        batch.setStatus(DocumentImportBatchStatus.RUNNING.name());
        batch.setTotalCount(1);
        batch.setTotalBytes((long) content.length);
        batch.setCreated(new Date());
        Mockito.when(context.batchAppService.requireOwnedBatch(
            BigInteger.ONE, created.getBatchId(), context.caller
        )).thenReturn(batch);

        context.facade.submit(
            context.caller,
            metadata,
            List.of(file("demo.txt", content))
        );

        ArgumentCaptor<DocumentImportBatchDtos.CreateRequest> request =
            ArgumentCaptor.forClass(DocumentImportBatchDtos.CreateRequest.class);
        Mockito.verify(context.batchAppService).createBatch(
            request.capture(), Mockito.any());
        Assert.assertEquals(
            Long.valueOf(content.length),
            request.getValue().getFiles().get(0).getFileSize()
        );
        Assert.assertNull(metadata.getFiles().get(0).getFileSize());
    }

    /**
     * 验证相同提交由服务端指纹复用，包含仍在上传的并发请求。
     */
    @Test
    public void submitShouldReuseTaskByServerFingerprint() {
        TestContext context = createContext();
        PublicDocumentImportDtos.BatchMetadata metadata =
            metadata("demo.txt", "folder/demo.txt", 4L);
        List<MultipartFile> files = List.of(file(
            "demo.txt",
            "demo".getBytes(StandardCharsets.UTF_8)
        ));

        DocumentImportBatchDtos.CreateResponse created =
            new DocumentImportBatchDtos.CreateResponse();
        created.setBatchId(BigInteger.valueOf(91));
        DocumentImportBatchDtos.ItemResponse createdItem =
            new DocumentImportBatchDtos.ItemResponse();
        createdItem.setItemId(BigInteger.valueOf(92));
        created.setItems(List.of(createdItem));
        Mockito.when(context.batchAppService.createBatch(
            Mockito.any(), Mockito.any()
        )).thenReturn(created);
        Mockito.when(context.itemService.updateById(Mockito.any()))
            .thenReturn(true);
        DocumentImportBatch batch = new DocumentImportBatch();
        batch.setId(created.getBatchId());
        batch.setKnowledgeId(BigInteger.ONE);
        batch.setStatus(DocumentImportBatchStatus.RUNNING.name());
        batch.setTotalCount(1);
        batch.setTotalBytes(4L);
        batch.setCreated(new Date());
        Mockito.when(context.batchAppService.requireOwnedBatch(
            BigInteger.ONE, created.getBatchId(), context.caller
        )).thenReturn(batch);

        context.facade.submit(context.caller, metadata, files);

        ArgumentCaptor<tech.easyflow.ai.entity.DocumentImportBatchItem> hashUpdate =
            ArgumentCaptor.forClass(tech.easyflow.ai.entity.DocumentImportBatchItem.class);
        Mockito.verify(context.itemService).updateById(hashUpdate.capture());
        Assert.assertNotNull(hashUpdate.getValue().getContentSha256());
        Assert.assertEquals(createdItem.getItemId(), hashUpdate.getValue().getId());
        ArgumentCaptor<DocumentImportBatchCreateContext> createContext =
            ArgumentCaptor.forClass(DocumentImportBatchCreateContext.class);
        Mockito.verify(context.batchAppService).createBatch(
            Mockito.any(), createContext.capture());
        batch.setRequestDigest(createContext.getValue().getRequestDigest());
        Mockito.when(context.batchService.getOne(
            Mockito.any(com.mybatisflex.core.query.QueryWrapper.class)))
            .thenReturn(batch);

        PublicDocumentImportDtos.SubmitResponse repeated =
            context.facade.submit(
                context.caller, metadata, files);

        Assert.assertEquals(batch.getId(), repeated.getTaskId());
        batch.setStatus(DocumentImportBatchStatus.UPLOADING.name());
        PublicDocumentImportDtos.SubmitResponse uploading =
            context.facade.submit(context.caller, metadata, files);

        Assert.assertEquals(batch.getId(), uploading.getTaskId());
        Mockito.verify(context.batchAppService, Mockito.times(1))
            .createBatch(Mockito.any(), Mockito.any());
    }

    /**
     * 验证长时间无进展的上传任务会先取消并释放指纹，再创建新任务。
     */
    @Test
    public void submitShouldReplaceStaleIncompleteTask() {
        TestContext context = createContext();
        PublicDocumentImportDtos.BatchMetadata metadata =
            metadata("demo.txt", "folder/demo.txt", 4L);
        List<MultipartFile> files = List.of(file(
            "demo.txt",
            "demo".getBytes(StandardCharsets.UTF_8)
        ));
        DocumentImportBatch stale = new DocumentImportBatch();
        stale.setId(BigInteger.valueOf(81));
        stale.setKnowledgeId(BigInteger.ONE);
        stale.setStatus(DocumentImportBatchStatus.UPLOADING.name());
        stale.setModified(new Date(
            System.currentTimeMillis() - 31L * 60L * 1000L
        ));
        Mockito.when(context.batchService.getOne(
            Mockito.any(com.mybatisflex.core.query.QueryWrapper.class)))
            .thenReturn(stale)
            .thenReturn(null);
        Mockito.when(context.batchAppService.cancelStaleBatch(
            Mockito.eq(BigInteger.ONE),
            Mockito.eq(stale.getId()),
            Mockito.eq(context.caller),
            Mockito.any(Date.class)
        )).thenReturn(true);
        Mockito.when(context.batchMapper.releaseSubmissionFingerprint(
            Mockito.eq(stale.getId()),
            Mockito.anyString(),
            Mockito.any(Date.class),
            Mockito.any(Date.class)
        )).thenReturn(1);

        DocumentImportBatchDtos.CreateResponse created =
            new DocumentImportBatchDtos.CreateResponse();
        created.setBatchId(BigInteger.valueOf(91));
        DocumentImportBatchDtos.ItemResponse item =
            new DocumentImportBatchDtos.ItemResponse();
        item.setItemId(BigInteger.valueOf(92));
        created.setItems(List.of(item));
        Mockito.when(context.batchAppService.createBatch(
            Mockito.any(), Mockito.any()
        )).thenReturn(created);
        Mockito.when(context.itemService.updateById(Mockito.any()))
            .thenReturn(true);
        DocumentImportBatch replacement = new DocumentImportBatch();
        replacement.setId(created.getBatchId());
        replacement.setKnowledgeId(BigInteger.ONE);
        replacement.setStatus(DocumentImportBatchStatus.RUNNING.name());
        replacement.setTotalCount(1);
        replacement.setTotalBytes(4L);
        replacement.setCreated(new Date());
        Mockito.when(context.batchAppService.requireOwnedBatch(
            BigInteger.ONE, created.getBatchId(), context.caller
        )).thenReturn(replacement);

        PublicDocumentImportDtos.SubmitResponse response =
            context.facade.submit(context.caller, metadata, files);

        Assert.assertEquals(replacement.getId(), response.getTaskId());
        Mockito.verify(context.batchAppService).cancelStaleBatch(
            Mockito.eq(BigInteger.ONE),
            Mockito.eq(stale.getId()),
            Mockito.eq(context.caller),
            Mockito.any(Date.class)
        );
        Mockito.verify(context.batchMapper).releaseSubmissionFingerprint(
            Mockito.eq(stale.getId()),
            Mockito.anyString(),
            Mockito.any(Date.class),
            Mockito.any(Date.class)
        );
    }

    /**
     * 验证显式空文件键数组会被拒绝，省略时由服务端重试全部异常文件。
     */
    @Test
    public void retryShouldDistinguishOmittedAndEmptyFileKeys() {
        TestContext context = createContext();
        PublicDocumentImportDtos.RetryRequest empty =
            new PublicDocumentImportDtos.RetryRequest();
        empty.setTaskId(BigInteger.ONE);
        empty.setFileKeys(List.of());

        try {
            context.facade.retry(context.caller, empty);
            Assert.fail("Expected empty fileKeys rejection");
        } catch (BusinessException expected) {
            Assert.assertTrue(expected.getMessage().contains("不能为空数组"));
        }

        PublicDocumentImportDtos.RetryRequest omitted =
            new PublicDocumentImportDtos.RetryRequest();
        omitted.setTaskId(BigInteger.ONE);
        tech.easyflow.ai.documentimport.DocumentImportBatchRetryResult result =
            new tech.easyflow.ai.documentimport.DocumentImportBatchRetryResult(
                BigInteger.ONE,
                DocumentImportBatchStatus.RUNNING.name(),
                1,
                2
            );
        Mockito.when(context.batchAppService.retryOwnedBatch(
            BigInteger.ONE,
            context.caller,
            java.util.Set.of()
        )).thenReturn(result);

        PublicDocumentImportDtos.RetryResponse response =
            context.facade.retry(context.caller, omitted);

        Assert.assertEquals(Integer.valueOf(2), response.getRetriedCount());
    }

    /**
     * 验证 Public API 将所有失败文件统一标记为可重试，不受历史标记影响。
     */
    @Test
    public void statusShouldExposeEveryFailedItemAsRetryable() {
        TestContext context = createContext();
        DocumentImportBatch batch = new DocumentImportBatch();
        batch.setId(BigInteger.ONE);
        batch.setKnowledgeId(BigInteger.TWO);
        batch.setStatus(DocumentImportBatchStatus.PARTIAL_SUCCEEDED.name());
        Mockito.when(context.batchAppService.requireBatchForCaller(
            BigInteger.ONE, context.caller
        )).thenReturn(batch);

        DocumentImportBatchDtos.StatusResponse internal =
            new DocumentImportBatchDtos.StatusResponse();
        internal.setTotalCount(1);
        internal.setFailedCount(1);
        internal.setRetryableFailedCount(1);
        internal.setProgressPercent(100);
        Mockito.when(context.batchTracker.toStatusResponse(batch))
            .thenReturn(internal);

        DocumentImportBatchItem failed = new DocumentImportBatchItem();
        failed.setBatchId(batch.getId());
        failed.setClientFileKey("failed.txt");
        failed.setStage(DocumentImportBatchItemStage.INDEX.name());
        failed.setStatus(DocumentImportBatchItemStatus.FAILED.name());
        failed.setRetryable(false);
        Mockito.when(context.itemService.page(
            Mockito.any(com.mybatisflex.core.paginate.Page.class),
            Mockito.any(com.mybatisflex.core.query.QueryWrapper.class)
        )).thenReturn(new com.mybatisflex.core.paginate.Page<>(
            List.of(failed), 1, 1, 1
        ));

        PublicDocumentImportDtos.StatusResponse response = context.facade.getStatus(
            context.caller, batch.getId(), null, 1, 20
        );

        Assert.assertTrue(response.getCanRetry());
        Assert.assertEquals(Integer.valueOf(1),
            response.getCounts().getRetryableFailed());
        Assert.assertTrue(response.getItems().getRecords().get(0).getRetryable());
    }

    /**
     * 创建单文件元数据。
     *
     * @param fileName 文件名
     * @param relativePath 相对路径
     * @param fileSize 文件大小
     * @return 元数据
     */
    private PublicDocumentImportDtos.BatchMetadata metadata(
        String fileName,
        String relativePath,
        long fileSize) {
        PublicDocumentImportDtos.BatchMetadata metadata =
            new PublicDocumentImportDtos.BatchMetadata();
        metadata.setKnowledgeId(BigInteger.ONE);
        metadata.setFiles(List.of(
            manifest(fileName, relativePath, fileSize)));
        return metadata;
    }

    /**
     * 创建清单项。
     *
     * @param fileName 文件名
     * @param relativePath 相对路径
     * @param fileSize 文件大小
     * @return 清单项
     */
    private DocumentImportBatchDtos.ManifestItem manifest(
        String fileName,
        String relativePath,
        long fileSize) {
        DocumentImportBatchDtos.ManifestItem item =
            new DocumentImportBatchDtos.ManifestItem();
        item.setClientFileKey(relativePath);
        item.setFileName(fileName);
        item.setRelativePath(relativePath);
        item.setFileSize(fileSize);
        return item;
    }

    /**
     * 创建可重复读取的 MultipartFile。
     *
     * @param fileName 文件名
     * @param bytes 文件内容
     * @return Multipart 文件
     */
    private MultipartFile file(String fileName, byte[] bytes) {
        MultipartFile file = Mockito.mock(MultipartFile.class);
        try {
            Mockito.when(file.getInputStream())
                .thenAnswer(invocation -> new ByteArrayInputStream(bytes));
        } catch (java.io.IOException error) {
            throw new IllegalStateException(error);
        }
        Mockito.when(file.getOriginalFilename()).thenReturn(fileName);
        Mockito.when(file.getSize()).thenReturn((long) bytes.length);
        Mockito.when(file.isEmpty()).thenReturn(bytes.length == 0);
        return file;
    }

    /**
     * 创建测试依赖。
     *
     * @return 测试上下文
     */
    private TestContext createContext() {
        DocumentImportBatchAppService batchAppService =
            Mockito.mock(DocumentImportBatchAppService.class);
        DocumentImportBatchTracker batchTracker =
            Mockito.mock(DocumentImportBatchTracker.class);
        DocumentImportBatchService batchService =
            Mockito.mock(DocumentImportBatchService.class);
        DocumentImportBatchItemService itemService =
            Mockito.mock(DocumentImportBatchItemService.class);
        DocumentImportBatchMapper batchMapper =
            Mockito.mock(DocumentImportBatchMapper.class);
        KnowledgeImportBatchFacade facade = new KnowledgeImportBatchFacade(
            batchAppService,
            batchTracker,
            batchService,
            itemService,
            batchMapper
        );
        return new TestContext(
            facade,
            batchAppService,
            batchTracker,
            batchService,
            itemService,
            batchMapper,
            new ImportCallerContext(
                ImportCallerType.PUBLIC_API,
                BigInteger.valueOf(77)
            )
        );
    }

    /**
     * 测试依赖集合。
     */
    private record TestContext(
        KnowledgeImportBatchFacade facade,
        DocumentImportBatchAppService batchAppService,
        DocumentImportBatchTracker batchTracker,
        DocumentImportBatchService batchService,
        DocumentImportBatchItemService itemService,
        DocumentImportBatchMapper batchMapper,
        ImportCallerContext caller
    ) {
    }
}
