package tech.easyflow.ai.easyagentsflow.upload;

import com.easyagents.flow.core.chain.repository.ChainStateRepository;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
import tech.easyflow.common.cache.RedisLockExecutor;
import tech.easyflow.common.filestorage.FileStorageService;
import tech.easyflow.common.filestorage.FileStorageWriteHandle;
import tech.easyflow.common.filestorage.FileStorageWriteResult;
import tech.easyflow.common.web.exceptions.BusinessException;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * {@link WorkflowApiUploadLifecycleService} multipart 文件生命周期测试。
 */
public class WorkflowApiUploadLifecycleServiceTest {

    private static final String HTTP_REQUEST_ID = "http-request-1";

    /**
     * 验证同名文件 Part 会按顺序保存并注入文件对象数组。
     */
    @Test
    public void prepareShouldStoreRepeatedFilePartsInOrder() {
        Fixture fixture = fixture();
        MultipartFile first = file("first.pdf", "application/pdf", 10L);
        MultipartFile second = file(
                "second.docx",
                "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                20L);
        Mockito.when(fixture.parameterResolver.resolveFileParameterNames("flow"))
                .thenReturn(Set.of("documents"));
        Mockito.when(fixture.parameterResolver.normalizeRuntimeVariables(
                        Mockito.eq("flow"),
                        Mockito.anyMap()))
                .thenAnswer(invocation -> new LinkedHashMap<>(
                        invocation.getArgument(1)));
        FileStorageWriteHandle firstHandle = handle("first.pdf");
        FileStorageWriteHandle secondHandle = handle("second.docx");
        Mockito.when(fixture.fileStorageService.prepareRecoverableWrite(
                        Mockito.anyString(),
                        Mockito.anyString()))
                .thenReturn(firstHandle, secondHandle);
        Mockito.when(fixture.fileStorageService.saveRecoverable(
                        first,
                        firstHandle))
                .thenReturn(new FileStorageWriteResult(
                        "/files/first.pdf",
                        firstHandle.encodeLocator()));
        Mockito.when(fixture.fileStorageService.saveRecoverable(
                        second,
                        secondHandle))
                .thenReturn(new FileStorageWriteResult(
                        "/files/second.docx",
                        secondHandle.encodeLocator()));

        WorkflowApiPreparedUpload prepared = fixture.service.prepare(
                HTTP_REQUEST_ID,
                "flow",
                Map.of("user_input", "解析"),
                Map.of("documents", List.of(first, second)));

        Assert.assertNotNull(prepared.getUploadId());
        Assert.assertEquals("解析", prepared.getVariables().get("user_input"));
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> documents =
                (List<Map<String, Object>>) prepared.getVariables()
                        .get("documents");
        Assert.assertEquals(2, documents.size());
        Assert.assertEquals(
                "/files/first.pdf",
                documents.get(0).get("filePath"));
        Assert.assertEquals(
                "/files/second.docx",
                documents.get(1).get("filePath"));

        ArgumentCaptor<WorkflowApiUploadRecord> recordCaptor =
                ArgumentCaptor.forClass(WorkflowApiUploadRecord.class);
        Mockito.verify(fixture.uploadStore).create(
                recordCaptor.capture());
        Assert.assertEquals(
                HTTP_REQUEST_ID,
                recordCaptor.getValue().getRequestId());
        Assert.assertNotEquals(
                recordCaptor.getValue().getRequestId(),
                recordCaptor.getValue().getUploadId());
        Assert.assertEquals(
                List.of("/files/first.pdf", "/files/second.docx"),
                recordCaptor.getValue().getStoredFiles().stream()
                        .map(WorkflowApiStoredFile::filePath)
                        .toList());
        InOrder writeOrder = Mockito.inOrder(
                fixture.uploadStore,
                fixture.fileStorageService);
        writeOrder.verify(fixture.uploadStore)
                .save(Mockito.any(WorkflowApiUploadRecord.class));
        writeOrder.verify(fixture.fileStorageService)
                .saveRecoverable(first, firstHandle);
    }

    /**
     * 验证非法客户端 MIME 会按扩展名归一化，并同时用于存储和文件描述。
     */
    @Test
    public void prepareShouldNormalizeInvalidContentType() {
        Fixture fixture = fixture();
        MultipartFile file = file(
                "C:\\fakepath\\report.docx",
                "Other",
                10L);
        Mockito.when(fixture.parameterResolver
                        .resolveFileParameterNames("flow"))
                .thenReturn(Set.of("documents"));
        Mockito.when(fixture.parameterResolver.normalizeRuntimeVariables(
                        Mockito.eq("flow"),
                        Mockito.anyMap()))
                .thenAnswer(invocation -> new LinkedHashMap<>(
                        invocation.getArgument(1)));
        FileStorageWriteHandle handle = handle("report.docx");
        Mockito.when(fixture.fileStorageService.prepareRecoverableWrite(
                        Mockito.anyString(),
                        Mockito.anyString()))
                .thenReturn(handle);
        Mockito.when(fixture.fileStorageService.saveRecoverable(
                        Mockito.any(MultipartFile.class),
                        Mockito.eq(handle)))
                .thenReturn(new FileStorageWriteResult(
                        "/files/report.docx",
                        handle.encodeLocator()));

        WorkflowApiPreparedUpload prepared = fixture.service.prepare(
                HTTP_REQUEST_ID,
                "flow",
                Map.of(),
                Map.of("documents", List.of(file)));

        ArgumentCaptor<MultipartFile> storedFile =
                ArgumentCaptor.forClass(MultipartFile.class);
        Mockito.verify(fixture.fileStorageService).saveRecoverable(
                storedFile.capture(),
                Mockito.eq(handle));
        Assert.assertEquals(
                "report.docx",
                storedFile.getValue().getOriginalFilename());
        Assert.assertEquals(
                "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                storedFile.getValue().getContentType());
        @SuppressWarnings("unchecked")
        List<Map<String, Object>> documents =
                (List<Map<String, Object>>) prepared.getVariables()
                        .get("documents");
        Assert.assertEquals(
                storedFile.getValue().getContentType(),
                documents.get(0).get("contentType"));
    }

    /**
     * 验证对象存储超时返回可重试的 50301，并补偿临时文件。
     */
    @Test
    public void prepareShouldTranslateStorageTimeoutAndCleanup() {
        Fixture fixture = fixture();
        MultipartFile file = file(
                "report.pdf",
                "application/pdf",
                10L);
        Mockito.when(fixture.parameterResolver
                        .resolveFileParameterNames("flow"))
                .thenReturn(Set.of("documents"));
        Mockito.when(fixture.parameterResolver.normalizeRuntimeVariables(
                        Mockito.eq("flow"),
                        Mockito.anyMap()))
                .thenAnswer(invocation -> new LinkedHashMap<>(
                        invocation.getArgument(1)));
        FileStorageWriteHandle handle = handle("report.pdf");
        Mockito.when(fixture.fileStorageService.prepareRecoverableWrite(
                        Mockito.anyString(),
                        Mockito.anyString()))
                .thenReturn(handle);
        Mockito.when(fixture.fileStorageService.saveRecoverable(
                        file,
                        handle))
                .thenThrow(new IllegalStateException(
                        "storage timeout",
                        new java.net.SocketTimeoutException("timeout")));

        BusinessException exception = Assert.assertThrows(
                BusinessException.class,
                () -> fixture.service.prepare(
                        HTTP_REQUEST_ID,
                        "flow",
                        Map.of(),
                        Map.of("documents", List.of(file))));

        Assert.assertEquals(503, exception.getHttpStatus());
        Assert.assertEquals(50301, exception.getErrorCode());
        Assert.assertFalse(exception.getMessage().contains("Socket"));
        Mockito.verify(fixture.fileStorageService)
                .deleteRecoverable(handle);
        Mockito.verify(fixture.uploadStore)
                .remove(Mockito.any(WorkflowApiUploadRecord.class));
    }

    /**
     * 验证存储鉴权等非暂时性错误返回安全的 50001，并执行补偿。
     */
    @Test
    public void prepareShouldHidePermanentStorageFailureAndCleanup() {
        Fixture fixture = fixture();
        MultipartFile file = file(
                "report.pdf",
                "application/pdf",
                10L);
        Mockito.when(fixture.parameterResolver
                        .resolveFileParameterNames("flow"))
                .thenReturn(Set.of("documents"));
        Mockito.when(fixture.parameterResolver.normalizeRuntimeVariables(
                        Mockito.eq("flow"),
                        Mockito.anyMap()))
                .thenAnswer(invocation -> new LinkedHashMap<>(
                        invocation.getArgument(1)));
        FileStorageWriteHandle handle = handle("report.pdf");
        Mockito.when(fixture.fileStorageService.prepareRecoverableWrite(
                        Mockito.anyString(),
                        Mockito.anyString()))
                .thenReturn(handle);
        Mockito.when(fixture.fileStorageService.saveRecoverable(
                        file,
                        handle))
                .thenThrow(new IllegalStateException(
                        "AccessKey=secret, endpoint=http://internal:9000"));

        BusinessException exception = Assert.assertThrows(
                BusinessException.class,
                () -> fixture.service.prepare(
                        HTTP_REQUEST_ID,
                        "flow",
                        Map.of(),
                        Map.of("documents", List.of(file))));

        Assert.assertEquals(500, exception.getHttpStatus());
        Assert.assertEquals(50001, exception.getErrorCode());
        Assert.assertFalse(exception.getMessage().contains("secret"));
        Mockito.verify(fixture.fileStorageService)
                .deleteRecoverable(handle);
        Mockito.verify(fixture.uploadStore)
                .remove(Mockito.any(WorkflowApiUploadRecord.class));
    }

    /**
     * 验证无法解析的存储主机按配置故障返回不可重试的 50001。
     */
    @Test
    public void prepareShouldTreatUnknownStorageHostAsPermanentFailure() {
        Fixture fixture = fixture();
        MultipartFile file = file(
                "report.pdf",
                "application/pdf",
                10L);
        Mockito.when(fixture.parameterResolver
                        .resolveFileParameterNames("flow"))
                .thenReturn(Set.of("documents"));
        Mockito.when(fixture.parameterResolver.normalizeRuntimeVariables(
                        Mockito.eq("flow"),
                        Mockito.anyMap()))
                .thenAnswer(invocation -> new LinkedHashMap<>(
                        invocation.getArgument(1)));
        FileStorageWriteHandle handle = handle("report.pdf");
        Mockito.when(fixture.fileStorageService.prepareRecoverableWrite(
                        Mockito.anyString(),
                        Mockito.anyString()))
                .thenReturn(handle);
        Mockito.when(fixture.fileStorageService.saveRecoverable(
                        file,
                        handle))
                .thenThrow(new IllegalStateException(
                        "storage endpoint unavailable",
                        new java.net.UnknownHostException(
                                "invalid-storage-host")));

        BusinessException exception = Assert.assertThrows(
                BusinessException.class,
                () -> fixture.service.prepare(
                        HTTP_REQUEST_ID,
                        "flow",
                        Map.of(),
                        Map.of("documents", List.of(file))));

        Assert.assertEquals(500, exception.getHttpStatus());
        Assert.assertEquals(50001, exception.getErrorCode());
        Assert.assertFalse(exception.getMessage().contains(
                "invalid-storage-host"));
        Mockito.verify(fixture.fileStorageService)
                .deleteRecoverable(handle);
    }

    /**
     * 验证未知文件 Part 在写入存储前被拒绝。
     */
    @Test
    public void prepareShouldRejectUnknownFilePartBeforeStorage() {
        Fixture fixture = fixture();
        MultipartFile file = file("data.pdf", "application/pdf", 10L);
        Mockito.when(fixture.parameterResolver.resolveFileParameterNames("flow"))
                .thenReturn(Set.of("documents"));

        BusinessException exception = Assert.assertThrows(
                BusinessException.class,
                () -> fixture.service.prepare(
                        HTTP_REQUEST_ID,
                        "flow",
                        Map.of(),
                        Map.of("unknown", List.of(file))));

        Assert.assertTrue(exception.getMessage().contains("unknown"));
        Mockito.verifyNoInteractions(fixture.fileStorageService);
        Mockito.verifyNoInteractions(fixture.uploadStore);
    }

    /**
     * 验证缺少开始节点必填文件字段时在存储前返回 40016。
     */
    @Test
    public void prepareShouldRejectMissingRequiredFileBeforeStorage() {
        Fixture fixture = fixture();
        MultipartFile file = file("data.pdf", "application/pdf", 10L);
        Mockito.when(fixture.parameterResolver
                        .resolveFileParameterNames("flow"))
                .thenReturn(Set.of("documents", "appendix"));
        Mockito.when(fixture.parameterResolver
                        .resolveRequiredFileParameterNames("flow"))
                .thenReturn(Set.of("documents", "appendix"));

        BusinessException exception = Assert.assertThrows(
                BusinessException.class,
                () -> fixture.service.prepare(
                        HTTP_REQUEST_ID,
                        "flow",
                        Map.of(),
                        Map.of("documents", List.of(file))));

        Assert.assertEquals(40016, exception.getErrorCode());
        Assert.assertTrue(exception.getMessage().contains("appendix"));
        Mockito.verifyNoInteractions(fixture.fileStorageService);
        Mockito.verifyNoInteractions(fixture.uploadStore);
    }

    /**
     * 验证文件数量和大小限制统一转换为 41301。
     */
    @Test
    public void prepareShouldTranslateFileLimitToPayloadTooLarge() {
        Fixture fixture = fixture();
        MultipartFile file = file("data.pdf", "application/pdf", 10L);
        Mockito.when(fixture.parameterResolver
                        .resolveFileParameterNames("flow"))
                .thenReturn(Set.of("documents"));
        Mockito.when(fixture.parameterResolver.normalizeRuntimeVariables(
                        Mockito.eq("flow"),
                        Mockito.anyMap()))
                .thenThrow(new BusinessException(
                        "文件参数 documents 最多上传 10 个文件"));

        BusinessException exception = Assert.assertThrows(
                BusinessException.class,
                () -> fixture.service.prepare(
                        HTTP_REQUEST_ID,
                        "flow",
                        Map.of(),
                        Map.of("documents", List.of(file))));

        Assert.assertEquals(413, exception.getHttpStatus());
        Assert.assertEquals(41301, exception.getErrorCode());
        Mockito.verifyNoInteractions(fixture.fileStorageService);
        Mockito.verifyNoInteractions(fixture.uploadStore);
    }

    /**
     * 验证文件写入后 Redis 更新失败时仍使用内存路径执行补偿删除。
     */
    @Test
    public void prepareShouldDeleteSavedFileWhenRecordUpdateFails() {
        Fixture fixture = fixture();
        MultipartFile file = file("data.pdf", "application/pdf", 10L);
        Mockito.when(fixture.parameterResolver.resolveFileParameterNames("flow"))
                .thenReturn(Set.of("documents"));
        Mockito.when(fixture.parameterResolver.normalizeRuntimeVariables(
                        Mockito.eq("flow"),
                        Mockito.anyMap()))
                .thenAnswer(invocation -> new LinkedHashMap<>(
                        invocation.getArgument(1)));
        FileStorageWriteHandle handle = handle("data.pdf");
        Mockito.when(fixture.fileStorageService.prepareRecoverableWrite(
                        Mockito.anyString(),
                        Mockito.anyString()))
                .thenReturn(handle);
        Mockito.when(fixture.fileStorageService.saveRecoverable(
                        file,
                        handle))
                .thenReturn(new FileStorageWriteResult(
                        "/files/data.pdf",
                        handle.encodeLocator()));
        Mockito.doNothing()
                .doThrow(new IllegalStateException("Redis 写入失败"))
                .when(fixture.uploadStore)
                .save(Mockito.any(WorkflowApiUploadRecord.class));

        IllegalStateException exception = Assert.assertThrows(
                IllegalStateException.class,
                () -> fixture.service.prepare(
                        HTTP_REQUEST_ID,
                        "flow",
                        Map.of(),
                        Map.of("documents", List.of(file))));

        Assert.assertTrue(exception.getMessage().contains("Redis"));
        Mockito.verify(fixture.fileStorageService)
                .deleteRecoverable(handle);
        Mockito.verify(fixture.uploadStore)
                .remove(Mockito.any(WorkflowApiUploadRecord.class));
    }

    /**
     * 验证启动失败后的 abort 会幂等删除已保存文件和上传记录。
     */
    @Test
    public void abortShouldDeleteStoredFilesAndRecord() {
        Fixture fixture = fixture();
        WorkflowApiUploadRecord record = new WorkflowApiUploadRecord();
        record.setUploadId("upload-1");
        record.setRequestId(HTTP_REQUEST_ID);
        FileStorageWriteHandle firstHandle = handle("a.pdf");
        FileStorageWriteHandle secondHandle = handle("b.pdf");
        record.setStoredFiles(List.of(
                new WorkflowApiStoredFile(
                        "/files/a.pdf",
                        firstHandle.encodeLocator()),
                new WorkflowApiStoredFile(
                        "/files/b.pdf",
                        secondHandle.encodeLocator())));
        RedisLockExecutor.LockHandle handle =
                Mockito.mock(RedisLockExecutor.LockHandle.class);
        Mockito.when(fixture.redisLockExecutor.tryAcquire(
                        Mockito.anyString(),
                        Mockito.any(),
                        Mockito.any()))
                .thenReturn(handle);
        Mockito.when(fixture.uploadStore.find("upload-1"))
                .thenReturn(Optional.of(record));

        fixture.service.abort("upload-1");

        Mockito.verify(fixture.fileStorageService)
                .deleteRecoverable(firstHandle);
        Mockito.verify(fixture.fileStorageService)
                .deleteRecoverable(secondHandle);
        Mockito.verify(fixture.uploadStore).remove(record);
        Mockito.verify(handle).close();
    }

    /**
     * 验证单条清理失败不会阻塞后续到期记录。
     */
    @Test
    public void cleanupExpiredShouldContinueAfterFailedRecord() {
        Fixture fixture = fixture();
        FileStorageWriteHandle failedFile = handle("failed.pdf");
        FileStorageWriteHandle goodFile = handle("good.pdf");
        WorkflowApiUploadRecord failedRecord =
                storedRecord("failed", failedFile);
        WorkflowApiUploadRecord goodRecord =
                storedRecord("good", goodFile);
        RedisLockExecutor.LockHandle failedLock =
                Mockito.mock(RedisLockExecutor.LockHandle.class);
        RedisLockExecutor.LockHandle goodLock =
                Mockito.mock(RedisLockExecutor.LockHandle.class);
        Mockito.when(fixture.uploadStore.claimExpired(
                        Mockito.anyLong(),
                        Mockito.anyLong()))
                .thenReturn(
                        Optional.of("failed"),
                        Optional.of("good"),
                        Optional.empty());
        Mockito.when(fixture.redisLockExecutor.tryAcquire(
                        Mockito.anyString(),
                        Mockito.any(),
                        Mockito.any()))
                .thenReturn(failedLock, goodLock);
        Mockito.when(fixture.uploadStore.find("failed"))
                .thenReturn(Optional.of(failedRecord));
        Mockito.when(fixture.uploadStore.find("good"))
                .thenReturn(Optional.of(goodRecord));
        Mockito.doThrow(new IllegalStateException("对象存储不可用"))
                .when(fixture.fileStorageService)
                .deleteRecoverable(failedFile);

        int processed = fixture.service.cleanupExpired(10);

        Assert.assertEquals(2, processed);
        Mockito.verify(fixture.fileStorageService)
                .deleteRecoverable(goodFile);
        Mockito.verify(fixture.uploadStore).remove(goodRecord);
    }

    /**
     * 创建 multipart 文件桩。
     *
     * @param name 文件名
     * @param contentType MIME 类型
     * @param size 文件大小
     * @return multipart 文件桩
     */
    private MultipartFile file(
            String name,
            String contentType,
            long size) {
        MultipartFile file = Mockito.mock(MultipartFile.class);
        Mockito.when(file.isEmpty()).thenReturn(false);
        Mockito.when(file.getOriginalFilename()).thenReturn(name);
        Mockito.when(file.getContentType()).thenReturn(contentType);
        Mockito.when(file.getSize()).thenReturn(size);
        return file;
    }

    /**
     * 创建可恢复文件存储句柄。
     *
     * @param filename 固定文件名
     * @return 测试句柄
     */
    private FileStorageWriteHandle handle(String filename) {
        return new FileStorageWriteHandle(
                "localFileStorage",
                "",
                "/tmp/easyflow-test",
                "workflow-api-upload/test",
                filename);
    }

    /**
     * 创建包含单个临时文件的上传记录。
     *
     * @param uploadId 内部上传 ID
     * @param handle 文件句柄
     * @return 上传记录
     */
    private WorkflowApiUploadRecord storedRecord(
            String uploadId,
            FileStorageWriteHandle handle) {
        WorkflowApiUploadRecord record =
                new WorkflowApiUploadRecord();
        record.setUploadId(uploadId);
        record.setRequestId(HTTP_REQUEST_ID);
        record.setStoredFiles(List.of(
                new WorkflowApiStoredFile(
                        "/files/" + handle.getFilename(),
                        handle.encodeLocator())));
        return record;
    }

    /**
     * 创建生命周期服务测试夹具。
     *
     * @return 测试夹具
     */
    private Fixture fixture() {
        WorkflowRunningParameterResolver parameterResolver =
                Mockito.mock(WorkflowRunningParameterResolver.class);
        FileStorageService fileStorageService =
                Mockito.mock(FileStorageService.class);
        WorkflowApiUploadStore uploadStore =
                Mockito.mock(WorkflowApiUploadStore.class);
        ChainStateRepository chainStateRepository =
                Mockito.mock(ChainStateRepository.class);
        RedisLockExecutor redisLockExecutor =
                Mockito.mock(RedisLockExecutor.class);
        return new Fixture(
                new WorkflowApiUploadLifecycleService(
                        parameterResolver,
                        fileStorageService,
                        new WorkflowApiMultipartFileNormalizer(),
                        uploadStore,
                        chainStateRepository,
                        redisLockExecutor),
                parameterResolver,
                fileStorageService,
                uploadStore,
                redisLockExecutor);
    }

    /**
     * 生命周期服务测试夹具。
     *
     * @param service 被测服务
     * @param parameterResolver 参数解析器
     * @param fileStorageService 文件存储
     * @param uploadStore 上传记录存储
     * @param redisLockExecutor 分布式锁执行器
     */
    private record Fixture(
            WorkflowApiUploadLifecycleService service,
            WorkflowRunningParameterResolver parameterResolver,
            FileStorageService fileStorageService,
            WorkflowApiUploadStore uploadStore,
            RedisLockExecutor redisLockExecutor) {
    }
}
