package com.easyagents.skill.validation.defaults;

import com.easyagents.skill.exception.SkillValidationException;
import com.easyagents.skill.factory.SkillFactory;
import com.easyagents.skill.model.Skill;
import com.easyagents.skill.model.SkillResource;
import com.easyagents.skill.model.SkillResourceKind;
import com.easyagents.skill.model.SkillPackageLimits;
import com.easyagents.skill.util.SkillHashes;
import com.easyagents.skill.validation.SkillValidationReport;
import com.easyagents.skill.validation.SkillValidationMode;
import com.easyagents.skill.validation.SkillValidationSeverity;
import org.junit.Assert;
import org.junit.Test;

import java.nio.charset.StandardCharsets;

/**
 * DefaultSkillValidator 单元测试。
 */
public class DefaultSkillValidatorTest {

    private final DefaultSkillValidator validator = new DefaultSkillValidator();

    /**
     * 完整标准 Skill 校验通过。
     */
    @Test
    public void validateCompleteSkill() {
        Skill skill = validSkill("skill-a");
        skill.setResources(java.util.List.of(
                textResource("references/a.md", SkillResourceKind.REFERENCE, "# A"),
                textResource("scripts/run.sh", SkillResourceKind.SCRIPT, "echo ok"),
                binaryResource("assets/a.bin", "abc")
        ));

        validator.validate(skill);
    }

    /**
     * 嵌套 Map、List、布尔和数字 frontmatter 可校验。
     */
    @Test
    public void allowNestedFrontmatter() {
        String content = "---\nname: nested-skill\ndescription: Handles nested metadata\n"
                + "metadata:\n  enabled: true\n  retries: 3\n  tags:\n    - alpha\n    - beta\n"
                + "---\n# Nested\n";
        Skill skill = SkillFactory.create(content);

        validator.validate(skill);

        Assert.assertTrue(skill.getDocument().getFrontmatter().get("metadata") instanceof java.util.Map);
    }

    /**
     * 结构化文档更新后名称与描述从 frontmatter 派生。
     */
    @Test
    public void applyEditedDocumentToAggregate() {
        Skill skill = validSkill("skill-a");
        com.easyagents.skill.model.SkillDocument document = skill.getDocument();
        document.putFrontmatter("description", "Updated description for the Skill");

        skill.setDocument(document);

        Assert.assertEquals("Updated description for the Skill", skill.getDescription());
        Assert.assertEquals("Updated description for the Skill",
                skill.getDocument().getFrontmatter().get("description"));
        validator.validate(skill);
    }

    /**
     * 历史下划线名称只产生 warning。
     */
    @Test
    public void legacyUnderscoreNameProducesWarning() {
        Skill skill = validSkill("legacy_skill");

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertFalse(report.hasErrors());
        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "NON_CANONICAL_NAME".equals(issue.getCode())
                        && issue.getSeverity() == SkillValidationSeverity.WARNING));
    }

    /**
     * 正式标准模式拒绝仅为草稿导入兼容的下划线名称。
     */
    @Test
    public void standardModeRejectsLegacyUnderscoreName() {
        Skill skill = validSkill("legacy_skill");

        SkillValidationReport report = validator.validateReport(
                skill, SkillPackageLimits.defaults(), SkillValidationMode.STANDARD);

        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "NON_CANONICAL_NAME".equals(issue.getCode())
                        && issue.getSeverity() == SkillValidationSeverity.ERROR));
    }

    /**
     * AgentScope 嵌套 metadata 保真并标记标准兼容 warning。
     */
    @Test
    public void nestedMetadataProducesCompatibilityWarning() {
        Skill skill = SkillFactory.create("---\nname: metadata-skill\n"
                + "description: Nested metadata compatibility\nmetadata:\n  provider:\n    enabled: true\n"
                + "---\n# Metadata\n");

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertFalse(report.hasErrors());
        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "NON_STANDARD_METADATA".equals(issue.getCode())));
    }

    /**
     * 标准可选字符串字段拒绝空值或非字符串。
     */
    @Test
    public void rejectInvalidOptionalStandardFields() {
        Skill skill = SkillFactory.create("---\nname: optional-skill\n"
                + "description: Invalid optional fields\nlicense: []\ncompatibility: ''\nallowed-tools:\n  - Read\n"
                + "---\n# Optional\n");

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "INVALID_LICENSE".equals(issue.getCode())
                        && Integer.valueOf(4).equals(issue.getLine())
                        && Integer.valueOf(1).equals(issue.getColumn())));
        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "INVALID_COMPATIBILITY".equals(issue.getCode())
                        && Integer.valueOf(5).equals(issue.getLine())
                        && Integer.valueOf(1).equals(issue.getColumn())));
        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "INVALID_ALLOWED_TOOLS".equals(issue.getCode())
                        && Integer.valueOf(6).equals(issue.getLine())
                        && Integer.valueOf(1).equals(issue.getColumn())));
    }

    /**
     * 非规范名称返回结构化错误。
     */
    @Test
    public void rejectInvalidName() {
        Skill skill = validSkill("Invalid Name");

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertTrue(report.hasErrors());
        Assert.assertTrue(report.getIssues().stream().anyMatch(issue -> "INVALID_NAME".equals(issue.getCode())));
    }

    /**
     * 名称与目录不一致时失败。
     */
    @Test(expected = SkillValidationException.class)
    public void rejectRootNameMismatch() {
        Skill skill = validSkill("skill-a");
        skill.setPackageRoot("other-skill");

        validator.validate(skill);
    }

    /**
     * 大小写冲突路径会被拒绝。
     */
    @Test
    public void rejectCaseConflictingResourcePaths() {
        Skill skill = validSkill("skill-a");
        skill.setResources(java.util.List.of(
                textResource("references/A.md", SkillResourceKind.REFERENCE, "A"),
                textResource("references/a.md", SkillResourceKind.REFERENCE, "B")
        ));

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "RESOURCE_PATH_COLLISION".equals(issue.getCode())));
    }

    /**
     * 文件资源不能同时作为另一个资源的祖先路径。
     */
    @Test
    public void rejectFileAndDescendantResourcePaths() {
        Skill skill = validSkill("skill-a");
        skill.setResources(java.util.List.of(
                textResource("references/a.md", SkillResourceKind.REFERENCE, "descendant"),
                textResource("references", SkillResourceKind.OTHER, "file")
        ));

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "RESOURCE_PATH_HIERARCHY_CONFLICT".equals(issue.getCode())));
    }

    /**
     * 路径穿越会被拒绝。
     */
    @Test
    public void rejectParentPath() {
        Skill skill = validSkill("skill-a");
        skill.setResources(java.util.List.of(
                textResource("references/../a.md", SkillResourceKind.REFERENCE, "A")));

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "UNSAFE_RESOURCE_PATH".equals(issue.getCode())));
    }

    /**
     * 缺失资源路径必须形成结构化问题，并继续聚合其他资源错误。
     */
    @Test
    public void aggregateMissingPathWithOtherResourceErrors() {
        Skill skill = validSkill("skill-a");
        SkillResource missingPath = textResource(
                "references/missing.md", SkillResourceKind.REFERENCE, "missing");
        missingPath.setPath(null);
        skill.setResources(java.util.List.of(
                missingPath,
                textResource("references/../escape.md", SkillResourceKind.REFERENCE, "escape")));

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "RESOURCE_PATH_REQUIRED".equals(issue.getCode()) && issue.getPath() == null));
        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "UNSAFE_RESOURCE_PATH".equals(issue.getCode())));
    }

    /**
     * 文本资源 hash 和 size 必须与 UTF-8 内容一致。
     */
    @Test
    public void rejectTextHashAndSizeMismatch() {
        Skill skill = validSkill("skill-a");
        SkillResource resource = textResource("references/a.md", SkillResourceKind.REFERENCE, "A");
        resource.setContentHash("0".repeat(64));
        resource.setSize(99);
        skill.setResources(java.util.List.of(resource));

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "RESOURCE_HASH_MISMATCH".equals(issue.getCode())));
        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "RESOURCE_SIZE_MISMATCH".equals(issue.getCode())));
    }

    /**
     * 文本扩展名必须使用严格 UTF-8 文本表示，与所在目录无关。
     */
    @Test
    public void rejectBinaryRepresentationForTextExtension() {
        Skill skill = validSkill("skill-a");
        SkillResource script = binaryResource("scripts/run.sh", "echo unsafe");
        script.setKind(SkillResourceKind.SCRIPT);
        skill.setResources(java.util.List.of(script));

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "RESOURCE_CONTENT_MODE_MISMATCH".equals(issue.getCode())
                        && issue.getSeverity() == SkillValidationSeverity.ERROR));
    }

    /**
     * assets 目录中的文本资源使用规范文本表示。
     */
    @Test
    public void acceptTextResourceInAssetsDirectory() {
        Skill skill = validSkill("skill-a");
        SkillResource textAsset = textResource("assets/readme.txt", SkillResourceKind.ASSET, "text asset");
        skill.setResources(java.util.List.of(textAsset));

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertFalse(report.hasErrors());
    }

    /**
     * scripts 目录允许保存不透明二进制辅助文件。
     */
    @Test
    public void acceptBinaryResourceInScriptsDirectory() {
        Skill skill = validSkill("skill-a");
        SkillResource binaryScript = binaryResource("scripts/helper.bin", "opaque");
        binaryScript.setKind(SkillResourceKind.SCRIPT);
        skill.setResources(java.util.List.of(binaryScript));

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertFalse(report.hasErrors());
    }

    /**
     * 空脚本允许保存在草稿中，但应返回可见 warning。
     */
    @Test
    public void warnForEmptyScriptResource() {
        Skill skill = validSkill("skill-a");
        skill.setResources(java.util.List.of(
                textResource("scripts/run.py", SkillResourceKind.SCRIPT, "")));

        SkillValidationReport report = validator.validateReport(skill);

        Assert.assertFalse(report.hasErrors());
        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "EMPTY_SCRIPT".equals(issue.getCode())
                        && issue.getSeverity() == SkillValidationSeverity.WARNING));
    }

    /**
     * 结构化校验器执行资源单文件安全限额。
     */
    @Test
    public void reportConfiguredResourceLimit() {
        Skill skill = validSkill("skill-a");
        skill.setResources(java.util.List.of(
                textResource("references/a.md", SkillResourceKind.REFERENCE, "x".repeat(300))));
        DefaultSkillValidator limitedValidator = new DefaultSkillValidator(
                SkillPackageLimits.builder().maxTextFileBytes(128).build());

        SkillValidationReport report = limitedValidator.validateReport(skill);

        Assert.assertTrue(report.getIssues().stream().anyMatch(issue ->
                "TEXT_FILE_SIZE_LIMIT".equals(issue.getCode())));
    }

    /**
     * 单次调用传入的限额优先于校验器构造时的默认限额。
     */
    @Test
    public void operationLimitsOverrideValidatorDefaults() {
        Skill skill = validSkill("skill-a");
        String path = "references/" + "a".repeat(520) + ".md";
        skill.setResources(java.util.List.of(
                textResource(path, SkillResourceKind.REFERENCE, "A")));
        SkillPackageLimits operationLimits = SkillPackageLimits.builder()
                .maxPathLength(600)
                .build();

        SkillValidationReport report = validator.validateReport(skill, operationLimits);

        Assert.assertFalse(report.hasErrors());
    }

    private static Skill validSkill(String name) {
        return SkillFactory.create(skillMd(name));
    }

    private static String skillMd(String name) {
        return "---\nname: " + name + "\ndescription: Use this Skill for validator tests\n---\n# Skill\n";
    }

    private static SkillResource textResource(String path, SkillResourceKind kind, String content) {
        byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
        SkillResource resource = new SkillResource();
        resource.setPath(path);
        resource.setKind(kind);
        resource.setMediaType("text/markdown");
        resource.setTextContent(content);
        resource.setContentHash(SkillHashes.sha256Hex(bytes));
        resource.setSize(bytes.length);
        return resource;
    }

    private static SkillResource binaryResource(String path, String content) {
        byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
        String hash = SkillHashes.sha256Hex(bytes);
        SkillResource resource = new SkillResource();
        resource.setPath(path);
        resource.setKind(SkillResourceKind.ASSET);
        resource.setMediaType("application/octet-stream");
        resource.setContentRef("sha256:" + hash);
        resource.setContentHash(hash);
        resource.setSize(bytes.length);
        return resource;
    }
}
