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.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.InputStream;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Set;
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.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
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())));
    }

    /**
     * 一个上传 ZIP 只能承载一个 Skill，批量导入由多个 token 独立完成。
     */
    @Test
    public void previewRejectsMultipleSkillsInOneZip() {
        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(multiSkillZip()));
        }

        assertTrue(preview.getSkills().isEmpty());
        assertTrue(preview.getIssues().stream()
                .anyMatch(issue -> "STANDARD_PACKAGE_SKILL_COUNT".equals(issue.getCode())));
    }

    /**
     * 私有 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);
        }
    }

    private byte[] multiSkillZip() {
        try {
            ByteArrayOutputStream bytes = new ByteArrayOutputStream();
            try (ZipOutputStream zip = new ZipOutputStream(bytes, StandardCharsets.UTF_8)) {
                write(zip, "alpha-skill/SKILL.md", skillContent("alpha-skill"));
                write(zip, "beta-skill/SKILL.md", skillContent("beta-skill"));
            }
            return bytes.toByteArray();
        } catch (Exception exception) {
            throw new IllegalStateException("创建多 Skill 测试包失败", exception);
        }
    }

    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);
        }
    }
}
