package tech.easyflow.skill.imports;

import com.mybatisflex.core.query.QueryWrapper;
import org.junit.Test;
import org.mockito.MockedStatic;
import org.springframework.web.multipart.MultipartFile;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.filestorage.FileStorageService;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.skill.entity.Skill;
import tech.easyflow.skill.entity.SkillImportStage;
import tech.easyflow.skill.service.SkillService;
import tech.easyflow.skill.store.DBSkillContentStore;
import tech.easyflow.system.service.ResourceAccessService;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * 标准 Skill ZIP 的导入导出契约测试。
 */
public class StandardSkillPackageContractTest {

    /**
     * 单个标准目录包可预检，并保留任意自定义资源目录。
     */
    @Test
    public void previewAcceptsOneStandardSkillWithCustomResources() {
        SkillService skillService = mock(SkillService.class);
        when(skillService.list(any(QueryWrapper.class))).thenReturn(List.of());
        SkillImportServiceImpl service = importService(skillService);

        SkillImportPreview preview;
        try (MockedStatic<SaTokenUtil> ignored = login()) {
            preview = service.previewStandardForTest(new ByteArrayInputStream(skillZip(
                    "demo-skill", "custom/prompts/system.txt")));
        }

        assertEquals(1, preview.getSkills().size());
        assertEquals("demo-skill", preview.getSkills().get(0).getName());
        assertTrue(preview.getSkills().get(0).getFiles().stream()
                .anyMatch(file -> "custom/prompts/system.txt".equals(file.getPath())));
    }

    /**
     * 一个父目录包装的多 Skill ZIP 会被拆成多个独立 token 预览。
     */
    @Test
    public void previewSplitsParentWrappedSkillsIntoIndependentTokens() {
        BatchFixture fixture = batchImportService();
        MultipartFile file = multipartFile("skill-bundle.zip", multiSkillZip("skill-bundle/", 2));

        List<SkillImportPreview> previews;
        try (MockedStatic<SaTokenUtil> ignored = login()) {
            previews = fixture.service().previewBatch(List.of(file));
        }

        assertEquals(2, previews.size());
        assertEquals(List.of("skill-1", "skill-2"), previews.stream()
                .map(preview -> preview.getSkills().get(0).getName()).toList());
        assertTrue(previews.stream().allMatch(preview -> preview.getSkills().size() == 1));
        assertTrue(previews.stream().allMatch(preview -> preview.getImportToken() != null));
        assertTrue(previews.stream().allMatch(preview -> "skill-bundle.zip".equals(preview.getSourceName())));
        verify(fixture.fileStorageService(), times(2)).save(any(java.io.File.class), anyString());
        verify(fixture.stageStore(), times(2)).create(anyString(), anyString());
    }

    /**
     * 单个 ZIP 解析出的 Skill 超过统一上限时，在创建暂存 token 前拒绝。
     */
    @Test
    public void previewRejectsMoreThanFiftySkills() {
        BatchFixture fixture = batchImportService();
        MultipartFile file = multipartFile("too-many.zip", multiSkillZip("", 51));

        BusinessException exception;
        try (MockedStatic<SaTokenUtil> ignored = login()) {
            exception = assertThrows(BusinessException.class,
                    () -> fixture.service().previewBatch(List.of(file)));
        }

        assertTrue(exception.getMessage().contains("50"));
        verify(fixture.fileStorageService(), times(0)).save(any(java.io.File.class), anyString());
        verify(fixture.stageStore(), times(0)).create(anyString(), anyString());
    }

    /**
     * 多个上传包合计超过五十个 Skill 时，已创建的独立 token 会全部回滚。
     */
    @Test
    public void previewEnforcesFiftySkillLimitAcrossUploadedPackages() {
        BatchFixture fixture = batchImportService();
        MultipartFile first = multipartFile("first.zip", multiSkillZip("", 25));
        MultipartFile second = multipartFile("second.zip", multiSkillZip("bundle/", 26));

        BusinessException exception;
        try (MockedStatic<SaTokenUtil> ignored = login()) {
            exception = assertThrows(BusinessException.class,
                    () -> fixture.service().previewBatch(List.of(first, second)));
        }

        assertTrue(exception.getMessage().contains("50"));
        verify(fixture.fileStorageService(), times(25)).save(any(java.io.File.class), anyString());
        verify(fixture.stageStore(), times(25)).create(anyString(), anyString());
        verify(fixture.stageStore(), times(25)).cancel(anyString());
    }

    /**
     * 私有 efskill 扩展名必须在读取前被拒绝。
     */
    @Test
    public void previewRejectsEfskillExtension() {
        MultipartFile file = mock(MultipartFile.class);
        when(file.isEmpty()).thenReturn(false);
        when(file.getSize()).thenReturn(10L);
        when(file.getOriginalFilename()).thenReturn("legacy.efskill");

        BusinessException exception = assertThrows(BusinessException.class,
                () -> importService(mock(SkillService.class)).preview(file));

        assertTrue(exception.getMessage().contains(".efskill 已停止支持"));
    }

    /**
     * 导出始终生成标准 ZIP，路径不暴露数据库 ID。
     */
    @Test
    public void exportProducesPortableStandardZip() {
        BigInteger id = new BigInteger("987654321012345678");
        Skill skill = new Skill();
        skill.setId(id);
        skill.setName("portable-skill");
        skill.setDescription("Portable skill");
        skill.setSkillContent(skillContent("portable-skill"));
        skill.setResources(List.of());
        SkillService skillService = mock(SkillService.class);
        when(skillService.getPackageDetail(id)).thenReturn(skill);
        SkillExportServiceImpl service = new SkillExportServiceImpl(
                skillService, mock(DBSkillContentStore.class));

        byte[] bytes;
        try (SkillExportArtifact artifact = service.prepare(List.of(id))) {
            ByteArrayOutputStream output = new ByteArrayOutputStream();
            artifact.transferTo(output);
            bytes = output.toByteArray();
        }

        Set<String> paths = zipPaths(bytes);
        assertTrue(paths.stream().anyMatch(path -> path.endsWith("portable-skill/SKILL.md")));
        assertTrue(paths.stream().noneMatch(path -> path.contains(id.toString())));
    }

    private SkillImportServiceImpl importService(SkillService skillService) {
        return new SkillImportServiceImpl(skillService, mock(DBSkillContentStore.class),
                mock(FileStorageService.class), mock(SkillImportStageStore.class),
                mock(ResourceAccessService.class));
    }

    private MockedStatic<SaTokenUtil> login() {
        LoginAccount account = new LoginAccount();
        account.setId(BigInteger.valueOf(7));
        account.setTenantId(BigInteger.ONE);
        MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class);
        saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
        return saToken;
    }

    private byte[] skillZip(String name, String resourcePath) {
        try {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            try (ZipOutputStream zip = new ZipOutputStream(bytes, StandardCharsets.UTF_8)) {
                write(zip, name + "/SKILL.md", skillContent(name));
                write(zip, name + "/" + resourcePath, "Use concise language.");
            }
            return bytes.toByteArray();
        } catch (Exception exception) {
            throw new IllegalStateException("创建标准 Skill 测试包失败", exception);
        }
    }

    /**
     * 创建包含指定数量 Skill 的测试 ZIP。
     *
     * @param parent 可选父目录前缀
     * @param count Skill 数量
     * @return ZIP 字节
     */
    private byte[] multiSkillZip(String parent, int count) {
        try {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            try (ZipOutputStream zip = new ZipOutputStream(bytes, StandardCharsets.UTF_8)) {
                for (int index = 1; index <= count; index++) {
                    String name = "skill-" + index;
                    write(zip, parent + name + "/SKILL.md", skillContent(name));
                }
            }
            return bytes.toByteArray();
        } catch (Exception exception) {
            throw new IllegalStateException("创建多 Skill 测试包失败", exception);
        }
    }

    /**
     * 创建带内存文件存储行为的批量导入测试夹具。
     *
     * @return 批量导入测试夹具
     */
    private BatchFixture batchImportService() {
        SkillService skillService = mock(SkillService.class);
        when(skillService.list(any(QueryWrapper.class))).thenReturn(List.of());
        FileStorageService fileStorageService = mock(FileStorageService.class);
        SkillImportStageStore stageStore = mock(SkillImportStageStore.class);
        Map<String, byte[]> storedFiles = new HashMap<>();
        AtomicInteger sequence = new AtomicInteger();
        try {
            when(fileStorageService.save(any(java.io.File.class), anyString())).thenAnswer(invocation -> {
                java.io.File file = invocation.getArgument(0);
                String path = "stored-" + sequence.incrementAndGet() + ".zip";
                storedFiles.put(path, Files.readAllBytes(file.toPath()));
                return path;
            });
            when(fileStorageService.readStream(anyString())).thenAnswer(invocation ->
                    new ByteArrayInputStream(storedFiles.get(invocation.getArgument(0))));
        } catch (IOException exception) {
            throw new IllegalStateException("创建测试文件存储失败", exception);
        }
        when(stageStore.create(anyString(), anyString())).thenAnswer(invocation -> {
            SkillImportStage stage = new SkillImportStage();
            stage.setFilePath(invocation.getArgument(0));
            stage.setOriginalName(invocation.getArgument(1));
            stage.setImportToken("token-" + sequence.get());
            stage.setExpiresAt(new Date(System.currentTimeMillis() + 60_000));
            return stage;
        });
        SkillImportServiceImpl service = new SkillImportServiceImpl(skillService,
                mock(DBSkillContentStore.class), fileStorageService, stageStore,
                mock(ResourceAccessService.class));
        return new BatchFixture(service, fileStorageService, stageStore);
    }

    /**
     * 创建可重复打开输入流的上传文件替身。
     *
     * @param name 原始文件名
     * @param bytes 文件字节
     * @return 上传文件替身
     */
    private MultipartFile multipartFile(String name, byte[] bytes) {
        MultipartFile file = mock(MultipartFile.class);
        when(file.isEmpty()).thenReturn(false);
        when(file.getSize()).thenReturn((long) bytes.length);
        when(file.getOriginalFilename()).thenReturn(name);
        try {
            when(file.getInputStream()).thenAnswer(ignored -> new ByteArrayInputStream(bytes));
        } catch (IOException exception) {
            throw new IllegalStateException("创建测试上传文件失败", exception);
        }
        return file;
    }

    private void write(ZipOutputStream zip, String path, String content) throws Exception {
        zip.putNextEntry(new ZipEntry(path));
        zip.write(content.getBytes(StandardCharsets.UTF_8));
        zip.closeEntry();
    }

    private String skillContent(String name) {
        return "---\nname: " + name + "\ndescription: Standard package fixture\n---\n# Instructions\n";
    }

    private Set<String> zipPaths(byte[] bytes) {
        try (InputStream input = new ByteArrayInputStream(bytes);
             ZipInputStream zip = new ZipInputStream(input, StandardCharsets.UTF_8)) {
            java.util.LinkedHashSet<String> paths = new java.util.LinkedHashSet<>();
            ZipEntry entry;
            while ((entry = zip.getNextEntry()) != null) {
                if (!entry.isDirectory()) {
                    paths.add(entry.getName());
                }
            }
            return paths.stream().collect(Collectors.toCollection(java.util.LinkedHashSet::new));
        } catch (Exception exception) {
            throw new IllegalStateException("读取标准 Skill 测试包失败", exception);
        }
    }

    /**
     * 批量预检测试依赖集合。
     *
     * @param service 导入服务
     * @param fileStorageService 文件存储
     * @param stageStore 导入暂存仓库
     */
    private record BatchFixture(SkillImportServiceImpl service,
                                FileStorageService fileStorageService,
                                SkillImportStageStore stageStore) {
    }
}
