package skills

import (
	"archive/tar"
	"archive/zip"
	"compress/gzip"
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/go-git/go-billy/v5"
	"github.com/go-git/go-billy/v5/memfs"
	git "github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing/object"
	"github.com/go-git/go-git/v5/plumbing/transport"
	"github.com/go-git/go-git/v5/plumbing/transport/client"
	gitserver "github.com/go-git/go-git/v5/plumbing/transport/server"
	"github.com/go-git/go-git/v5/storage/memory"
	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/service/resource/repos"
)

func TestScanSkillRepositoryRecognizesEverySkillDirectory(t *testing.T) {
	root := t.TempDir()
	writeSkill(t, root, "SKILL.md", "root-sk", "Root skill")
	writeSkill(t, root, "tools/SKILL.md", "tools-sk", "Tools skill")
	writeSkill(t, root, "skills/backend/SKILL.md", "backend-sk", "Backend skill")
	writeSkill(t, root, ".agents/skills/reviewer/SKILL.md", "reviewer-sk", "Reviewer skill")
	writeSkill(t, root, "packages/tools/skills/deploy/SKILL.md", "deploy-sk", "Deploy skill")
	writeSkill(t, root, "deep/nested/SKILL.md", "nested-sk", "Nested skill")

	preview, err := ScanSkillRepository(root, 200)
	require.NoError(t, err)
	paths := make([]string, 0, len(preview.DetectedSkills))
	for _, row := range preview.DetectedSkills {
		paths = append(paths, row.Path)
		require.Equal(t, SkillRepositorySkillStatusValid, row.Status, row.Path)
	}
	require.ElementsMatch(t, []string{"SKILL.md", "tools/SKILL.md", "skills/backend/SKILL.md", ".agents/skills/reviewer/SKILL.md", "packages/tools/skills/deploy/SKILL.md", "deep/nested/SKILL.md"}, paths)
	layouts := map[string]string{}
	for _, row := range preview.DetectedSkills {
		layouts[row.Path] = row.Layout
	}
	require.Equal(t, SkillRepositoryLayoutRoot, layouts["SKILL.md"])
	require.Equal(t, SkillRepositoryLayoutCollection, layouts["tools/SKILL.md"])
	require.Equal(t, SkillRepositoryLayoutTemplate, layouts["skills/backend/SKILL.md"])
	require.Equal(t, SkillRepositoryLayoutTemplate, layouts[".agents/skills/reviewer/SKILL.md"])
	require.Equal(t, SkillRepositoryLayoutTemplate, layouts["packages/tools/skills/deploy/SKILL.md"])
	require.Equal(t, SkillRepositoryLayoutCollection, layouts["deep/nested/SKILL.md"])
}

func TestScanSkillRepositoryAllowsDisplayNamesAndDerivesSafeDirectoryNames(t *testing.T) {
	root := t.TempDir()
	writeSkill(t, root, "tidb/SKILL.md", "TiDB SQL Optimization KB", "TiDB helper")
	writeSkill(t, root, "zh/SKILL.md", "优化知识库", "Chinese helper")
	writeSkill(t, root, "cpp/SKILL.md", "C++ tuning!", "Punctuation helper")
	writeSkill(t, root, "root-only/SKILL.md", "知识库", "Path fallback helper")
	writeFile(t, root, "SKILL.md", "---\nname: \"知识库\"\ndescription: Root fallback helper\n---\n")

	preview, err := ScanSkillRepository(root, 200)
	require.NoError(t, err)
	byPath := map[string]SkillRepositoryDetectedSkill{}
	for _, row := range preview.DetectedSkills {
		byPath[row.Path] = row
		require.Equal(t, SkillRepositorySkillStatusValid, row.Status, row.Path)
	}
	require.Equal(t, "TiDB SQL Optimization KB", byPath["tidb/SKILL.md"].Name)
	require.Equal(t, "tidb-sql-optimization-kb", skillRepositoryDirectoryNameForSkill(byPath["tidb/SKILL.md"].Name, byPath["tidb/SKILL.md"].Path))
	require.Equal(t, "优化知识库", byPath["zh/SKILL.md"].Name)
	require.Equal(t, "zh", skillRepositoryDirectoryNameForSkill(byPath["zh/SKILL.md"].Name, byPath["zh/SKILL.md"].Path))
	require.Equal(t, "C++ tuning!", byPath["cpp/SKILL.md"].Name)
	require.Equal(t, "c-tuning", skillRepositoryDirectoryNameForSkill(byPath["cpp/SKILL.md"].Name, byPath["cpp/SKILL.md"].Path))
	require.Equal(t, "知识库", byPath["root-only/SKILL.md"].Name)
	require.Equal(t, "root-only", skillRepositoryDirectoryNameForSkill(byPath["root-only/SKILL.md"].Name, byPath["root-only/SKILL.md"].Path))
	require.Equal(t, "知识库", byPath["SKILL.md"].Name)
	require.Regexp(t, `^skill-[0-9a-f]{16}$`, skillRepositoryDirectoryNameForSkill(byPath["SKILL.md"].Name, byPath["SKILL.md"].Path))
	require.Equal(t, byPath["SKILL.md"].Name, preview.SuggestedName)
	require.Equal(t, skillRepositoryDirectoryNameForSkill(byPath["SKILL.md"].Name, byPath["SKILL.md"].Path), preview.SuggestedSlug)
}

func TestScanSkillRepositoryRejectsDisplayNamesWithPathSafetyRisks(t *testing.T) {
	root := t.TempDir()
	cases := map[string]struct {
		content  string
		wantName string
	}{
		"slash/SKILL.md":     {content: "---\nname: 'bad/name'\ndescription: Unsafe helper\n---\n", wantName: "bad/name"},
		"backslash/SKILL.md": {content: "---\nname: 'bad\\name'\ndescription: Unsafe helper\n---\n", wantName: `bad\name`},
		"dotdot/SKILL.md":    {content: "---\nname: 'bad..name'\ndescription: Unsafe helper\n---\n", wantName: "bad..name"},
		"control/SKILL.md":   {content: "---\nname: \"bad\\x01name\"\ndescription: Unsafe helper\n---\n", wantName: "bad\u0001name"},
	}
	for path, tc := range cases {
		writeFile(t, root, path, tc.content)
	}

	preview, err := ScanSkillRepository(root, 200)
	require.NoError(t, err)
	require.Len(t, preview.DetectedSkills, len(cases))
	byPath := map[string]SkillRepositoryDetectedSkill{}
	for _, row := range preview.DetectedSkills {
		byPath[row.Path] = row
		require.Equal(t, SkillRepositorySkillStatusRejected, row.Status, row.Path)
		require.Equal(t, SkillRepositoryIssueFrontmatterInvalid, row.IssueCode, row.Path)
	}
	for path, tc := range cases {
		require.Equal(t, tc.wantName, byPath[path].Name, path)
	}
}

func TestScanSkillRepositoryRejectsMissingDescriptionOversizeSymlinkAndDuplicateName(t *testing.T) {
	root := t.TempDir()
	writeSkill(t, root, "one/SKILL.md", "same-sk", "One")
	writeSkill(t, root, "two/SKILL.md", "same-sk", "Two")
	writeSkill(t, root, "display-one/SKILL.md", "Display One", "First display skill")
	writeSkill(t, root, "display-two/SKILL.md", "Display-One", "Second display skill")
	writeFile(t, root, "nodesc/SKILL.md", "---\nname: nodesc-sk\n---\n")
	writeFile(t, root, "big/SKILL.md", "---\nname: big-sk\ndescription: Big\n---\n"+strings.Repeat("x", SkillRepositoryMaxSkillMDBytes))
	require.NoError(t, os.Symlink(filepath.Join(root, "one"), filepath.Join(root, "link")))

	preview, err := ScanSkillRepository(root, 200)
	require.NoError(t, err)
	issues := map[string]string{}
	for _, row := range preview.DetectedSkills {
		issues[row.Path] = row.IssueCode
	}
	require.Equal(t, SkillRepositoryIssueDuplicateName, issues["one/SKILL.md"])
	require.Equal(t, SkillRepositoryIssueDuplicateName, issues["two/SKILL.md"])
	require.Equal(t, SkillRepositoryIssueDuplicateName, issues["display-one/SKILL.md"])
	require.Equal(t, SkillRepositoryIssueDuplicateName, issues["display-two/SKILL.md"])
	require.Equal(t, SkillRepositoryIssueMissingDescription, issues["nodesc/SKILL.md"])
	require.Equal(t, SkillRepositoryIssueOversize, issues["big/SKILL.md"])
	require.Equal(t, SkillRepositoryIssueSymlink, issues["link"])
}

func TestScanSkillRepositoryCapsRows(t *testing.T) {
	root := t.TempDir()
	writeSkill(t, root, "one/SKILL.md", "one-sk", "One")
	writeSkill(t, root, "two/SKILL.md", "two-sk", "Two")
	preview, err := ScanSkillRepository(root, 1)
	require.NoError(t, err)
	require.True(t, preview.Truncated)
	require.Equal(t, SkillImportErrorScanLimitExceeded, preview.IssueCode)
	require.Len(t, preview.DetectedSkills, 1)
}

func TestReadSkillRepositorySkillsReturnsRequestedValidContentFromExpectedSnapshot(t *testing.T) {
	storage := memory.NewStorage()
	filesystem := memfs.New()
	repo, err := git.Init(storage, filesystem)
	require.NoError(t, err)
	require.NoError(t, filesystem.MkdirAll("skills/valid", 0o755))
	require.NoError(t, filesystem.MkdirAll("skills/second", 0o755))
	require.NoError(t, filesystem.MkdirAll("skills/rejected", 0o755))
	writeGitTestFile(t, filesystem, "skills/valid/SKILL.md", "---\nname: valid-sk\ndescription: Valid skill\n---\n")
	writeGitTestFile(t, filesystem, "skills/second/SKILL.md", "---\nname: second-sk\ndescription: Second skill\n---\n")
	writeGitTestFile(t, filesystem, "skills/rejected/SKILL.md", "---\nname: rejected-sk\n---\n")
	writeGitTestFile(t, filesystem, "README.md", "private notes")
	worktree, err := repo.Worktree()
	require.NoError(t, err)
	_, err = worktree.Add(".")
	require.NoError(t, err)
	commit, err := worktree.Commit("seed", &git.CommitOptions{Author: &object.Signature{Name: "test", Email: "test@example.com", When: time.Now()}})
	require.NoError(t, err)

	const scheme = "skillcontenttest"
	endpoint, err := transport.NewEndpoint(scheme + "://ags.test/usr_owner/reviewer.sk.git")
	require.NoError(t, err)
	client.InstallProtocol(scheme, gitserver.NewClient(gitserver.MapLoader{endpoint.String(): storage}))
	t.Cleanup(func() { client.InstallProtocol(scheme, nil) })
	manager := GoGitRepositoryContentManager{BaseURL: scheme + "://ags.test"}
	repository := repos.RepositoryIdentity{FullName: "usr_owner/reviewer.sk", DefaultBranch: "master"}

	content, err := manager.ReadSkillRepositorySkills(context.Background(), repository, SkillRepositoryContentsRequest{
		ExpectedCommitSHA: commit.String(),
		Paths:             []string{"skills/second/SKILL.md", "skills/valid/SKILL.md", "skills/second/SKILL.md"},
	})
	require.NoError(t, err)
	require.Equal(t, commit.String(), content.CommitSHA)
	require.Equal(t, []string{"second-sk", "valid-sk"}, []string{content.Items[0].Name, content.Items[1].Name})
	require.Equal(t, []string{"skills/second/SKILL.md", "skills/valid/SKILL.md"}, []string{content.Items[0].Path, content.Items[1].Path})
}

func TestReadSkillRepositorySkillsFailsClosedForSnapshotAndInvalidPaths(t *testing.T) {
	manager, repository, commit := newSkillRepositoryContentManagerForTest(t)
	_, err := manager.ReadSkillRepositorySkills(context.Background(), repository, SkillRepositoryContentsRequest{
		ExpectedCommitSHA: strings.Repeat("0", 40),
		Paths:             []string{"skills/valid/SKILL.md"},
	})
	require.ErrorIs(t, err, ErrConflict)
	require.Equal(t, SkillRepositoryErrorSnapshotChanged, Code(err))

	for _, path := range []string{"README.md", "../README.md", "skills/rejected/SKILL.md"} {
		_, err := manager.ReadSkillRepositorySkills(context.Background(), repository, SkillRepositoryContentsRequest{
			ExpectedCommitSHA: commit,
			Paths:             []string{"skills/valid/SKILL.md", path},
		})
		require.ErrorIs(t, err, ErrNotFound, path)
	}
}

func TestReadSkillRepositorySkillsValidatesRequestBounds(t *testing.T) {
	manager, repository, commit := newSkillRepositoryContentManagerForTest(t)
	for _, request := range []SkillRepositoryContentsRequest{
		{ExpectedCommitSHA: "", Paths: []string{"skills/valid/SKILL.md"}},
		{ExpectedCommitSHA: "abc123", Paths: []string{"skills/valid/SKILL.md"}},
		{ExpectedCommitSHA: commit},
		{ExpectedCommitSHA: commit, Paths: make([]string, SkillRepositoryContentsPathLimit+1)},
		{ExpectedCommitSHA: commit, Paths: []string{strings.Repeat("x", SkillRepositoryContentsPathMaxBytes+1)}},
	} {
		_, err := manager.ReadSkillRepositorySkills(context.Background(), repository, request)
		require.ErrorIs(t, err, ErrInvalidArgument)
	}
}

func TestImportSkillRepositoryReplacesExistingSkillsWhenRequested(t *testing.T) {
	manager, repository, _ := newSkillRepositoryContentManagerWithSetupForTest(t, "skillimportreplace", func(filesystem billy.Filesystem) {
		require.NoError(t, filesystem.MkdirAll("skills/agentteam-workflow-plan-author-skills", 0o755))
		writeGitTestFile(t, filesystem, "skills/agentteam-workflow-plan-author-skills/SKILL.md", "---\nname: AgentTeam Workflow Plan Author\ndescription: Scaffold placeholder\n---\n")
		writeGitTestFile(t, filesystem, "README.md", "before\n<!-- BEGIN agentteam skill index -->\n| Skill | Entry | Purpose | Owner |\n| --- | --- | --- | --- |\n| AgentTeam Workflow Plan Author | skills/agentteam-workflow-plan-author-skills/SKILL.md | Scaffold placeholder |  |\n<!-- END agentteam skill index -->\nafter\n")
	})
	archivePath := filepath.Join(t.TempDir(), "workflow-skills.tar.gz")
	writeTarGz(t, archivePath, map[string]string{
		"skills/workflow-plan-author/SKILL.md": "---\nname: workflow-plan-author\ndescription: Workflow helper\n---\n",
	})

	result, err := manager.ImportSkillRepository(context.Background(), SkillRepositoryImportRequest{
		TargetRepository: repository,
		SourceFilePath:   archivePath,
		SourceName:       "agentteam-workflow-plan-author-skills.tar.gz",
		ReplaceSkills:    true,
	})
	require.NoError(t, err)
	require.Equal(t, 1, result.SkillCount)
	require.Equal(t, []string{"workflow-plan-author"}, detectedSkillNames(result.ImportedSkills))

	preview, err := manager.ListSkillRepositorySkills(context.Background(), repository)
	require.NoError(t, err)
	require.Equal(t, []string{"workflow-plan-author"}, detectedSkillNames(validDetectedSkills(preview.DetectedSkills)))
	require.Equal(t, "skills/workflow-plan-author/SKILL.md", preview.DetectedSkills[0].Path)
}

func TestReadSkillRepositorySkillsFailsClosedForSymlink(t *testing.T) {
	manager, repository, commit := newSkillRepositoryContentManagerWithSetupForTest(t, "skillcontentsymlink", func(filesystem billy.Filesystem) {
		require.NoError(t, filesystem.MkdirAll("skills/valid", 0o755))
		require.NoError(t, filesystem.MkdirAll("skills/link", 0o755))
		writeGitTestFile(t, filesystem, "skills/valid/SKILL.md", "---\nname: valid-sk\ndescription: Valid skill\n---\n")
		require.NoError(t, filesystem.Symlink("../valid/SKILL.md", "skills/link/SKILL.md"))
	})

	result, err := manager.ReadSkillRepositorySkills(context.Background(), repository, SkillRepositoryContentsRequest{
		ExpectedCommitSHA: commit,
		Paths:             []string{"skills/valid/SKILL.md", "skills/link/SKILL.md"},
	})
	require.ErrorIs(t, err, ErrNotFound)
	require.Empty(t, result.Items)
}

func TestReadSkillRepositorySkillsFailsClosedForTruncatedScan(t *testing.T) {
	manager, repository, commit := newSkillRepositoryContentManagerWithSetupForTest(t, "skillcontenttruncated", func(filesystem billy.Filesystem) {
		for index := 0; index <= SkillRepositoryScanLimit; index++ {
			name := fmt.Sprintf("skill-%03d", index)
			path := fmt.Sprintf("skills/%s/SKILL.md", name)
			require.NoError(t, filesystem.MkdirAll(filepath.Dir(path), 0o755))
			writeGitTestFile(t, filesystem, path, fmt.Sprintf("---\nname: %s\ndescription: Skill %03d\n---\n", name, index))
		}
	})

	result, err := manager.ReadSkillRepositorySkills(context.Background(), repository, SkillRepositoryContentsRequest{
		ExpectedCommitSHA: commit,
		Paths:             []string{"skills/skill-000/SKILL.md"},
	})
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.Equal(t, SkillImportErrorScanLimitExceeded, Code(err))
	require.Empty(t, result.Items)
}

func newSkillRepositoryContentManagerForTest(t *testing.T) (GoGitRepositoryContentManager, repos.RepositoryIdentity, string) {
	t.Helper()
	return newSkillRepositoryContentManagerWithSetupForTest(t, "skillcontentbounds", func(filesystem billy.Filesystem) {
		require.NoError(t, filesystem.MkdirAll("skills/valid", 0o755))
		require.NoError(t, filesystem.MkdirAll("skills/rejected", 0o755))
		writeGitTestFile(t, filesystem, "skills/valid/SKILL.md", "---\nname: valid-sk\ndescription: Valid skill\n---\n")
		writeGitTestFile(t, filesystem, "skills/rejected/SKILL.md", "---\nname: rejected-sk\n---\n")
		writeGitTestFile(t, filesystem, "README.md", "private notes")
	})
}

func newSkillRepositoryContentManagerWithSetupForTest(t *testing.T, scheme string, setup func(billy.Filesystem)) (GoGitRepositoryContentManager, repos.RepositoryIdentity, string) {
	t.Helper()
	storage := memory.NewStorage()
	filesystem := memfs.New()
	repo, err := git.Init(storage, filesystem)
	require.NoError(t, err)
	setup(filesystem)
	worktree, err := repo.Worktree()
	require.NoError(t, err)
	_, err = worktree.Add(".")
	require.NoError(t, err)
	commit, err := worktree.Commit("seed", &git.CommitOptions{Author: &object.Signature{Name: "test", Email: "test@example.com", When: time.Now()}})
	require.NoError(t, err)

	endpoint, err := transport.NewEndpoint(scheme + "://ags.test/usr_owner/reviewer.sk.git")
	require.NoError(t, err)
	client.InstallProtocol(scheme, gitserver.NewClient(gitserver.MapLoader{endpoint.String(): storage}))
	t.Cleanup(func() { client.InstallProtocol(scheme, nil) })
	return GoGitRepositoryContentManager{BaseURL: scheme + "://ags.test"},
		repos.RepositoryIdentity{FullName: "usr_owner/reviewer.sk", DefaultBranch: "master"},
		commit.String()
}

func TestSkillTemplateFilesUseSkillsDirectoryAndRepositoryGuides(t *testing.T) {
	files := skillTemplateFiles("backend-sk", "Backend Skill", "Backend helpers", "zh-CN")
	require.Len(t, files, 4)
	byPath := map[string]string{}
	for _, file := range files {
		byPath[file.path] = file.content
	}
	require.Contains(t, byPath["README.md"], "## 技能索引")
	require.Contains(t, byPath["README.md"], "当前模板会创建下方的初始技能")
	require.Contains(t, byPath["README.md"], "| Backend Skill | skills/backend-sk/SKILL.md | Backend helpers |  |")
	require.Contains(t, byPath["README.md"], "导入后的维护流程")
	require.Contains(t, byPath["AGENTS.md"], "技能仓库")
	require.Contains(t, byPath["AGENTS.md"], "README.md")
	require.Contains(t, byPath["AGENTS.md"], "创建一个初始技能")
	require.Contains(t, byPath["skills/README.md"], "后续创建或导入")
	require.Contains(t, byPath["skills/README.md"], "skills/<skill-directory>/")
	require.Contains(t, byPath["skills/README.md"], "展示名称")
	require.Contains(t, byPath["skills/backend-sk/SKILL.md"], "name: Backend Skill")
	require.Contains(t, byPath["skills/backend-sk/SKILL.md"], "description: Backend helpers")
	require.NotContains(t, byPath, "skills/backend-sk/references/README.md")
}

func TestSkillTemplateFilesUseJapaneseRepositoryGuides(t *testing.T) {
	files := skillTemplateFiles("review-sk", "Review Skill", "レビュー支援", "ja")
	byPath := map[string]string{}
	for _, file := range files {
		byPath[file.path] = file.content
	}
	require.Contains(t, byPath["README.md"], "## スキル索引")
	require.Contains(t, byPath["README.md"], "初期スキルを作成します")
	require.Contains(t, byPath["README.md"], "| Review Skill | skills/review-sk/SKILL.md | レビュー支援 |  |")
	require.Contains(t, byPath["README.md"], "## インポート後のメンテナンス手順")
	require.Contains(t, byPath["AGENTS.md"], "スキルリポジトリ管理ルール")
	require.Contains(t, byPath["AGENTS.md"], "初期スキルを 1 つ作成")
	require.Contains(t, byPath["skills/README.md"], "今後作成またはインポート")
	require.Contains(t, byPath["skills/README.md"], "skills/<skill-directory>/")
	require.Contains(t, byPath["skills/README.md"], "表示名")
	require.NotContains(t, byPath["README.md"], "Repository structure")
	require.Contains(t, byPath["skills/review-sk/SKILL.md"], "name: Review Skill")
	require.Contains(t, byPath["skills/review-sk/SKILL.md"], "description: レビュー支援")
	require.NotContains(t, byPath, "skills/review-sk/references/README.md")
}

func TestValidateSkillImportURLRejectsCredentialsUnsupportedSchemeAndUnsafeHosts(t *testing.T) {
	ctx := context.Background()
	_, err := validateSkillImportURL(ctx, "https://user:secret@example.test/skills.zip")
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.Equal(t, SkillImportErrorInvalidSource, Code(err))

	_, err = validateSkillImportURL(ctx, "ftp://example.test/skills.zip")
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.Equal(t, SkillImportErrorUnsupportedScheme, Code(err))

	_, err = validateSkillImportURL(ctx, "https://127.0.0.1/skills.zip")
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.Equal(t, SkillImportErrorUnsafeRemoteHost, Code(err))
}

func TestResolveSkillImportURLMatchesDesktopHosts(t *testing.T) {
	cases := map[string]string{
		"https://github.com/user/repo":                             "https://github.com/user/repo/archive/refs/heads/main.zip",
		"https://github.com/user/repo/tree/feature/tools":          "https://github.com/user/repo/archive/refs/heads/feature/tools.zip",
		"https://github.com/user/repo.git":                         "https://github.com/user/repo/archive/refs/heads/main.zip",
		"https://github.com/user/repo/archive/refs/heads/main.zip": "https://github.com/user/repo/archive/refs/heads/main.zip",
		"git@gitlab.com:user/repo.git":                             "https://gitlab.com/user/repo/-/archive/main/repo-main.zip",
		"https://gitlab.com/group/sub/repo/-/tree/develop":         "https://gitlab.com/group/sub/repo/-/archive/develop/repo-develop.zip",
		"https://gitee.com/user/repo/tree/v2":                      "https://gitee.com/user/repo/repository/archive/v2.zip",
		"git@gitcode.com:user/repo.git":                            "https://gitcode.com/user/repo/-/archive/main/repo-main.zip",
		"https://example.com/skills/review.md":                     "https://example.com/skills/review.md",
	}
	for input, want := range cases {
		t.Run(input, func(t *testing.T) {
			got, err := resolveSkillImportURL(input)
			require.NoError(t, err)
			require.Equal(t, want, got)
		})
	}
}

func TestImportSourceArchiveExtractsSupportedSkillLayouts(t *testing.T) {
	root := t.TempDir()
	archivePath := filepath.Join(root, "skills.tar.gz")
	writeTarGz(t, archivePath, map[string]string{
		"bundle/skills/backend/SKILL.md":          "---\nname: backend-sk\ndescription: Backend helper\n---\n",
		"bundle/skills/backend/assets/readme.txt": "asset",
		"bundle/tooling/review/SKILL.md":          "---\nname: review-sk\ndescription: Review helper\n---\n",
	})

	sourceDir, cleanup, sourceName, warnings, err := importSourceArchive(context.Background(), SkillRepositoryImportRequest{
		SourceFilePath: archivePath,
	}, root)
	defer cleanup()
	require.NoError(t, err)
	require.Empty(t, warnings)
	require.Equal(t, "skills.tar.gz", sourceName)

	preview, err := ScanSkillRepository(sourceDir, SkillRepositoryScanLimit)
	require.NoError(t, err)
	require.Len(t, preview.DetectedSkills, 2)
	byPath := map[string]SkillRepositoryDetectedSkill{}
	for _, row := range preview.DetectedSkills {
		byPath[row.Path] = row
	}
	require.Equal(t, SkillRepositorySkillStatusValid, byPath["bundle/skills/backend/SKILL.md"].Status)
	require.Equal(t, "backend-sk", byPath["bundle/skills/backend/SKILL.md"].Name)
	require.Equal(t, SkillRepositorySkillStatusValid, byPath["bundle/tooling/review/SKILL.md"].Status)
	require.Equal(t, "review-sk", byPath["bundle/tooling/review/SKILL.md"].Name)
}

func TestImportSourceArchiveExtractsDirectMarkdown(t *testing.T) {
	root := t.TempDir()
	sourcePath := filepath.Join(root, "review.md")
	require.NoError(t, os.WriteFile(sourcePath, []byte("---\nname: review-sk\ndescription: Review helper\n---\n"), 0o644))

	sourceDir, cleanup, sourceName, warnings, err := importSourceArchive(context.Background(), SkillRepositoryImportRequest{
		SourceFilePath: sourcePath,
		SourceName:     "review.md",
	}, root)
	defer cleanup()
	require.NoError(t, err)
	require.Empty(t, warnings)
	require.Equal(t, "review.md", sourceName)

	preview, err := ScanSkillRepository(sourceDir, SkillRepositoryScanLimit)
	require.NoError(t, err)
	require.Len(t, preview.DetectedSkills, 1)
	require.Equal(t, SkillRepositorySkillStatusValid, preview.DetectedSkills[0].Status)
	require.Equal(t, "review-sk", preview.DetectedSkills[0].Name)
	require.Equal(t, "SKILL.md", preview.DetectedSkills[0].Path)
}

func TestImportSourceArchiveSkipsNonSkillSpecialZipEntries(t *testing.T) {
	root := t.TempDir()
	archivePath := filepath.Join(root, "skills.zip")
	file, err := os.Create(archivePath)
	require.NoError(t, err)
	zw := zip.NewWriter(file)
	special := &zip.FileHeader{Name: "bundle/AGENTS.md"}
	special.SetMode(os.ModeIrregular | 0o644)
	writer, err := zw.CreateHeader(special)
	require.NoError(t, err)
	_, err = writer.Write([]byte("ignored"))
	require.NoError(t, err)
	writer, err = zw.Create("bundle/skills/review/SKILL.md")
	require.NoError(t, err)
	_, err = writer.Write([]byte("---\nname: review-sk\ndescription: Review helper\n---\n"))
	require.NoError(t, err)
	require.NoError(t, zw.Close())
	require.NoError(t, file.Close())

	sourceDir, cleanup, _, warnings, err := importSourceArchive(context.Background(), SkillRepositoryImportRequest{
		SourceFilePath: archivePath,
		SourceName:     "skills.zip",
	}, root)
	defer cleanup()
	require.NoError(t, err)
	require.Empty(t, warnings)

	preview, err := ScanSkillRepository(sourceDir, SkillRepositoryScanLimit)
	require.NoError(t, err)
	require.Len(t, preview.DetectedSkills, 1)
	require.Equal(t, "review-sk", preview.DetectedSkills[0].Name)
}

func TestImportSourceArchiveSkipsGitMetadataZipEntries(t *testing.T) {
	root := t.TempDir()
	archivePath := filepath.Join(root, "skills.zip")
	file, err := os.Create(archivePath)
	require.NoError(t, err)
	zw := zip.NewWriter(file)
	for name, content := range map[string]string{
		"repo-main/.git/config":                         "[core]\n",
		"repo-main/skills/review/.git/hooks/post-merge": "ignored",
		"repo-main/skills/review/SKILL.md":              "---\nname: review-sk\ndescription: Review helper\n---\n",
		"repo-main/skills/review/references/guide.md":   "# Guide\n",
	} {
		writer, err := zw.Create(name)
		require.NoError(t, err)
		_, err = writer.Write([]byte(content))
		require.NoError(t, err)
	}
	require.NoError(t, zw.Close())
	require.NoError(t, file.Close())

	sourceDir, cleanup, _, warnings, err := importSourceArchive(context.Background(), SkillRepositoryImportRequest{
		SourceFilePath: archivePath,
		SourceName:     "skills.zip",
	}, root)
	defer cleanup()
	require.NoError(t, err)
	require.Empty(t, warnings)

	preview, err := ScanSkillRepository(sourceDir, SkillRepositoryScanLimit)
	require.NoError(t, err)
	require.Len(t, preview.DetectedSkills, 1)
	require.Equal(t, "repo-main/skills/review/SKILL.md", preview.DetectedSkills[0].Path)
	require.Equal(t, "review-sk", preview.DetectedSkills[0].Name)
	require.NoDirExists(t, filepath.Join(sourceDir, "repo-main", ".git"))
	require.NoDirExists(t, filepath.Join(sourceDir, "repo-main", "skills", "review", ".git"))
	require.FileExists(t, filepath.Join(sourceDir, "repo-main", "skills", "review", "references", "guide.md"))
}

func TestValidateSkillImportScanIgnoresInvalidCandidatesWhenValidSkillsExist(t *testing.T) {
	err := validateSkillImportScan(SkillRepositoryScanData{DetectedSkills: []SkillRepositoryDetectedSkill{
		{Name: "valid-sk", Description: "Valid", Path: "skills/valid/SKILL.md", Status: SkillRepositorySkillStatusValid, IssueCode: SkillRepositoryIssueOK},
		{Name: "missing-desc", Path: "skills/bad/SKILL.md", Status: SkillRepositorySkillStatusRejected, IssueCode: SkillRepositoryIssueMissingDescription},
	}})
	require.NoError(t, err)
}

func TestValidateSkillImportScanExplainsSkillFileRequirements(t *testing.T) {
	err := validateSkillImportScan(SkillRepositoryScanData{DetectedSkills: []SkillRepositoryDetectedSkill{
		{Path: "SKILL.md", Status: SkillRepositorySkillStatusRejected, IssueCode: SkillRepositoryIssueMissingName},
	}})
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.Equal(t, SkillImportErrorNoSkillsFound, Code(err))
	require.Contains(t, err.Error(), "SKILL.md")
	require.Contains(t, err.Error(), "frontmatter")
	require.Contains(t, err.Error(), "name")
	require.Contains(t, err.Error(), "description")
}

func TestCopySkillTreeCopiesNestedDetectedSkillRoot(t *testing.T) {
	root := t.TempDir()
	sourceDir := filepath.Join(root, "source")
	writeSkill(t, sourceDir, "repo-main/tooling/review/SKILL.md", "review-sk", "Review helper")
	writeFile(t, sourceDir, "repo-main/tooling/review/references/guide.md", "# Guide\n")
	writeFile(t, sourceDir, "repo-main/tooling/sibling.txt", "do not copy")

	sourceRoot := detectedSkillRoot(sourceDir, "repo-main/tooling/review/SKILL.md")
	targetRoot := filepath.Join(root, "repo", "skills", "review-sk")
	require.NoError(t, copySkillTree(sourceRoot, targetRoot, nil))

	require.FileExists(t, filepath.Join(targetRoot, "SKILL.md"))
	require.FileExists(t, filepath.Join(targetRoot, "references", "guide.md"))
	require.NoFileExists(t, filepath.Join(targetRoot, "..", "sibling.txt"))
	require.NoFileExists(t, filepath.Join(targetRoot, "sibling.txt"))
}

func TestImportScannedSkillsDoesNotAbsorbNestedSkillRoots(t *testing.T) {
	root := t.TempDir()
	sourceDir := filepath.Join(root, "source")
	writeSkill(t, sourceDir, "repo-main/SKILL.md", "parent-sk", "Parent helper")
	writeFile(t, sourceDir, "repo-main/notes.md", "parent note")
	writeSkill(t, sourceDir, "repo-main/nested/SKILL.md", "nested-sk", "Nested helper")
	writeFile(t, sourceDir, "repo-main/nested/references/guide.md", "nested guide")

	scan, err := ScanSkillRepository(sourceDir, SkillRepositoryScanLimit)
	require.NoError(t, err)
	targetRepoDir := filepath.Join(root, "repo")
	targetSkillsDir := filepath.Join(targetRepoDir, "skills")

	imported, err := importScannedSkills(sourceDir, targetSkillsDir, scan, false)
	require.NoError(t, err)
	require.Len(t, imported, 2)
	require.FileExists(t, filepath.Join(targetSkillsDir, "parent-sk", "SKILL.md"))
	require.FileExists(t, filepath.Join(targetSkillsDir, "parent-sk", "notes.md"))
	require.NoFileExists(t, filepath.Join(targetSkillsDir, "parent-sk", "nested", "SKILL.md"))
	require.NoFileExists(t, filepath.Join(targetSkillsDir, "parent-sk", "nested", "references", "guide.md"))
	require.FileExists(t, filepath.Join(targetSkillsDir, "nested-sk", "SKILL.md"))
	require.FileExists(t, filepath.Join(targetSkillsDir, "nested-sk", "references", "guide.md"))

	importedPreview, err := ScanSkillRepository(targetRepoDir, SkillRepositoryScanLimit)
	require.NoError(t, err)
	require.Len(t, importedPreview.DetectedSkills, 2)
	for _, row := range importedPreview.DetectedSkills {
		require.Equal(t, SkillRepositorySkillStatusValid, row.Status, row.Path)
	}
}

func TestImportScannedSkillsAppendsAndReplacesByName(t *testing.T) {
	root := t.TempDir()
	sourceDir := filepath.Join(root, "source")
	writeSkill(t, sourceDir, "bundle/replace/SKILL.md", "replace-sk", "Replacement helper")
	writeFile(t, sourceDir, "bundle/replace/references/new.md", "new guide")
	writeSkill(t, sourceDir, "bundle/new/SKILL.md", "new-sk", "New helper")

	targetRepoDir := filepath.Join(root, "repo")
	targetSkillsDir := filepath.Join(targetRepoDir, "skills")
	writeFile(t, targetRepoDir, "README.md", "before\n<!-- BEGIN agentteam skill index -->\n| Skill | Entry | Purpose | Owner |\n| --- | --- | --- | --- |\n| replace-sk | skills/replace-sk/SKILL.md | Old helper |  |\n<!-- END agentteam skill index -->\nafter\n")
	writeSkill(t, targetSkillsDir, "existing-sk/SKILL.md", "existing-sk", "Existing helper")
	writeFile(t, targetSkillsDir, "existing-sk/references/old.md", "keep me")
	writeSkill(t, targetSkillsDir, "replace-sk/SKILL.md", "replace-sk", "Old helper")
	writeFile(t, targetSkillsDir, "replace-sk/references/old.md", "remove me")

	scan, err := ScanSkillRepository(sourceDir, SkillRepositoryScanLimit)
	require.NoError(t, err)

	imported, err := importScannedSkills(sourceDir, targetSkillsDir, scan, false)
	require.NoError(t, err)
	require.ElementsMatch(t, []string{"replace-sk", "new-sk"}, detectedSkillNames(imported))

	require.FileExists(t, filepath.Join(targetSkillsDir, "existing-sk", "SKILL.md"))
	require.FileExists(t, filepath.Join(targetSkillsDir, "existing-sk", "references", "old.md"))
	require.FileExists(t, filepath.Join(targetSkillsDir, "replace-sk", "SKILL.md"))
	require.FileExists(t, filepath.Join(targetSkillsDir, "replace-sk", "references", "new.md"))
	require.NoFileExists(t, filepath.Join(targetSkillsDir, "replace-sk", "references", "old.md"))
	require.FileExists(t, filepath.Join(targetSkillsDir, "new-sk", "SKILL.md"))

	mergedPreview, err := ScanSkillRepository(targetRepoDir, SkillRepositoryScanLimit)
	require.NoError(t, err)
	mergedSkills := validDetectedSkills(mergedPreview.DetectedSkills)
	require.ElementsMatch(t, []string{"existing-sk", "replace-sk", "new-sk"}, detectedSkillNames(mergedSkills))
	require.NoError(t, updateReadmeSkillIndex(targetRepoDir, mergedSkills))
	readme, err := os.ReadFile(filepath.Join(targetRepoDir, "README.md"))
	require.NoError(t, err)
	require.Contains(t, string(readme), "| existing-sk | skills/existing-sk/SKILL.md | Existing helper |  |")
	require.Contains(t, string(readme), "| replace-sk | skills/replace-sk/SKILL.md | Replacement helper |  |")
	require.Contains(t, string(readme), "| new-sk | skills/new-sk/SKILL.md | New helper |  |")
	require.NotContains(t, string(readme), "Old helper")
}

func TestImportScannedSkillsCanReplaceEntireSkillSet(t *testing.T) {
	root := t.TempDir()
	sourceDir := filepath.Join(root, "source")
	writeSkill(t, sourceDir, "bundle/workflow/SKILL.md", "workflow-plan-author", "Workflow helper")

	targetRepoDir := filepath.Join(root, "repo")
	targetSkillsDir := filepath.Join(targetRepoDir, "skills")
	writeSkill(t, targetSkillsDir, "agentteam-workflow-plan-author-skills/SKILL.md", "AgentTeam Workflow Plan Author", "Scaffold placeholder")
	writeSkill(t, targetSkillsDir, "stale-official-skill/SKILL.md", "stale-official-skill", "Stale helper")

	scan, err := ScanSkillRepository(sourceDir, SkillRepositoryScanLimit)
	require.NoError(t, err)

	imported, err := importScannedSkills(sourceDir, targetSkillsDir, scan, true)
	require.NoError(t, err)
	require.Equal(t, []string{"workflow-plan-author"}, detectedSkillNames(imported))
	require.FileExists(t, filepath.Join(targetSkillsDir, "workflow-plan-author", "SKILL.md"))
	require.NoDirExists(t, filepath.Join(targetSkillsDir, "agentteam-workflow-plan-author-skills"))
	require.NoDirExists(t, filepath.Join(targetSkillsDir, "stale-official-skill"))
}

func TestImportScannedSkillsUsesSafeDirectoryNameForDisplayNames(t *testing.T) {
	root := t.TempDir()
	sourceDir := filepath.Join(root, "source")
	writeSkill(t, sourceDir, "bundle/review/SKILL.md", "Review Skill", "Review helper")
	writeFile(t, sourceDir, "bundle/review/references/guide.md", "guide")

	targetRepoDir := filepath.Join(root, "repo")
	targetSkillsDir := filepath.Join(targetRepoDir, "skills")
	writeFile(t, targetRepoDir, "README.md", "before\n<!-- BEGIN agentteam skill index -->\n| Skill | Entry | Purpose | Owner |\n| --- | --- | --- | --- |\n<!-- END agentteam skill index -->\nafter\n")

	scan, err := ScanSkillRepository(sourceDir, SkillRepositoryScanLimit)
	require.NoError(t, err)

	imported, err := importScannedSkills(sourceDir, targetSkillsDir, scan, false)
	require.NoError(t, err)
	require.Len(t, imported, 1)
	require.Equal(t, "Review Skill", imported[0].Name)
	require.FileExists(t, filepath.Join(targetSkillsDir, "review-skill", "SKILL.md"))
	require.FileExists(t, filepath.Join(targetSkillsDir, "review-skill", "references", "guide.md"))
	require.NoDirExists(t, filepath.Join(targetSkillsDir, "Review Skill"))

	mergedPreview, err := ScanSkillRepository(targetRepoDir, SkillRepositoryScanLimit)
	require.NoError(t, err)
	mergedSkills := validDetectedSkills(mergedPreview.DetectedSkills)
	require.NoError(t, updateReadmeSkillIndex(targetRepoDir, mergedSkills))
	readme, err := os.ReadFile(filepath.Join(targetRepoDir, "README.md"))
	require.NoError(t, err)
	require.Contains(t, string(readme), "| Review Skill | skills/review-skill/SKILL.md | Review helper |  |")
}

func TestImportScannedSkillsNormalizesSkillFilenameCase(t *testing.T) {
	root := t.TempDir()
	sourceDir := filepath.Join(root, "source")
	writeFile(t, sourceDir, "repo-main/tooling/skill.md", "---\nname: lower-sk\ndescription: Lowercase skill filename\n---\n")

	scan, err := ScanSkillRepository(sourceDir, SkillRepositoryScanLimit)
	require.NoError(t, err)
	targetSkillsDir := filepath.Join(root, "repo", "skills")

	imported, err := importScannedSkills(sourceDir, targetSkillsDir, scan, false)
	require.NoError(t, err)
	require.Len(t, imported, 1)
	require.FileExists(t, filepath.Join(targetSkillsDir, "lower-sk", "SKILL.md"))
	entries, err := os.ReadDir(filepath.Join(targetSkillsDir, "lower-sk"))
	require.NoError(t, err)
	names := make([]string, 0, len(entries))
	for _, entry := range entries {
		names = append(names, entry.Name())
	}
	require.Contains(t, names, "SKILL.md")
	require.NotContains(t, names, "skill.md")
}

func detectedSkillNames(rows []SkillRepositoryDetectedSkill) []string {
	names := make([]string, 0, len(rows))
	for _, row := range rows {
		names = append(names, row.Name)
	}
	return names
}

func TestExtractSkillZipRejectsPathTraversal(t *testing.T) {
	root := t.TempDir()
	archivePath := filepath.Join(root, "unsafe.zip")
	file, err := os.Create(archivePath)
	require.NoError(t, err)
	zw := zip.NewWriter(file)
	writer, err := zw.Create("../SKILL.md")
	require.NoError(t, err)
	_, err = writer.Write([]byte("---\nname: bad\ndescription: bad\n---\n"))
	require.NoError(t, err)
	require.NoError(t, zw.Close())
	require.NoError(t, file.Close())

	_, err = extractSkillSource(archivePath, "unsafe.zip", filepath.Join(root, "out"))
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.Equal(t, SkillImportErrorArchiveUnsafe, Code(err))
}

func TestExtractSkillZipRejectsWindowsPathSeparator(t *testing.T) {
	root := t.TempDir()
	archivePath := filepath.Join(root, "unsafe.zip")
	file, err := os.Create(archivePath)
	require.NoError(t, err)
	zw := zip.NewWriter(file)
	writer, err := zw.Create(`skill\SKILL.md`)
	require.NoError(t, err)
	_, err = writer.Write([]byte("---\nname: bad\ndescription: bad\n---\n"))
	require.NoError(t, err)
	require.NoError(t, zw.Close())
	require.NoError(t, file.Close())

	_, err = extractSkillSource(archivePath, "unsafe.zip", filepath.Join(root, "out"))
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.Equal(t, SkillImportErrorArchiveUnsafe, Code(err))
}

func TestExtractSkillZipSkipsOversizedEntryWithWarning(t *testing.T) {
	root := t.TempDir()
	archivePath := filepath.Join(root, "oversized.zip")
	out := filepath.Join(root, "out")
	file, err := os.Create(archivePath)
	require.NoError(t, err)
	zw := zip.NewWriter(file)
	for name, content := range map[string][]byte{
		"repo-main/skills/review/SKILL.md":                                   []byte("---\nname: review-sk\ndescription: Review helper\n---\n"),
		"repo-main/browse/test/fixtures/security-bench-haiku-responses.json": make([]byte, int(maxSkillArchiveEntryBytes)+1),
	} {
		writer, err := zw.Create(name)
		require.NoError(t, err)
		_, err = writer.Write(content)
		require.NoError(t, err)
	}
	require.NoError(t, zw.Close())
	require.NoError(t, file.Close())

	warnings, err := extractSkillSource(archivePath, "oversized.zip", out)
	require.NoError(t, err)
	require.Len(t, warnings, 1)
	require.Equal(t, SkillImportWarningArchiveEntrySkippedOversize, warnings[0].Code)
	require.Equal(t, "repo-main/browse/test/fixtures/security-bench-haiku-responses.json", warnings[0].Path)
	require.Equal(t, maxSkillArchiveEntryBytes+1, warnings[0].SizeBytes)
	require.Equal(t, maxSkillArchiveEntryBytes, warnings[0].LimitBytes)
	require.FileExists(t, filepath.Join(out, "repo-main", "skills", "review", "SKILL.md"))
	require.NoFileExists(t, filepath.Join(out, "repo-main", "browse", "test", "fixtures", "security-bench-haiku-responses.json"))
}

func TestExtractSkillTarSkipsOversizedEntryWithWarning(t *testing.T) {
	root := t.TempDir()
	archivePath := filepath.Join(root, "oversized.tar")
	out := filepath.Join(root, "out")
	file, err := os.Create(archivePath)
	require.NoError(t, err)
	tw := tar.NewWriter(file)
	skillContent := []byte("---\nname: review-sk\ndescription: Review helper\n---\n")
	require.NoError(t, tw.WriteHeader(&tar.Header{Name: "repo-main/skills/review/SKILL.md", Typeflag: tar.TypeReg, Mode: 0o644, Size: int64(len(skillContent))}))
	_, err = tw.Write(skillContent)
	require.NoError(t, err)
	require.NoError(t, tw.WriteHeader(&tar.Header{Name: "repo-main/fixtures/large.json", Typeflag: tar.TypeReg, Mode: 0o644, Size: maxSkillArchiveEntryBytes + 1}))
	_, err = tw.Write(make([]byte, int(maxSkillArchiveEntryBytes)+1))
	require.NoError(t, err)
	require.NoError(t, tw.Close())
	require.NoError(t, file.Close())

	warnings, err := extractSkillSource(archivePath, "oversized.tar", out)
	require.NoError(t, err)
	require.Len(t, warnings, 1)
	require.Equal(t, SkillImportWarningArchiveEntrySkippedOversize, warnings[0].Code)
	require.Equal(t, "repo-main/fixtures/large.json", warnings[0].Path)
	require.Equal(t, maxSkillArchiveEntryBytes+1, warnings[0].SizeBytes)
	require.Equal(t, maxSkillArchiveEntryBytes, warnings[0].LimitBytes)
	require.FileExists(t, filepath.Join(out, "repo-main", "skills", "review", "SKILL.md"))
	require.NoFileExists(t, filepath.Join(out, "repo-main", "fixtures", "large.json"))
}

func TestExtractSkillZipClassifiesEntryCountAsLimitExceeded(t *testing.T) {
	root := t.TempDir()
	archivePath := filepath.Join(root, "too-many.zip")
	file, err := os.Create(archivePath)
	require.NoError(t, err)
	zw := zip.NewWriter(file)
	for _, name := range []string{"one/SKILL.md", "two/SKILL.md"} {
		writer, err := zw.Create(name)
		require.NoError(t, err)
		_, err = writer.Write([]byte("---\nname: bad\ndescription: bad\n---\n"))
		require.NoError(t, err)
	}
	require.NoError(t, zw.Close())
	require.NoError(t, file.Close())

	_, err = extractSkillZip(archivePath, filepath.Join(root, "out"), skillArchiveLimits{
		maxEntries:        1,
		maxEntryBytes:     maxSkillArchiveEntryBytes,
		maxExtractedBytes: maxSkillArchiveExtractedBytes,
	})
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.Equal(t, SkillImportErrorArchiveLimitExceeded, Code(err))
	require.Contains(t, err.Error(), "too many entries")
}

func TestExtractSkillTarClassifiesEntryCountAsLimitExceeded(t *testing.T) {
	root := t.TempDir()
	archivePath := filepath.Join(root, "too-many.tar")
	file, err := os.Create(archivePath)
	require.NoError(t, err)
	tw := tar.NewWriter(file)
	for _, name := range []string{"one/SKILL.md", "two/SKILL.md"} {
		content := []byte("---\nname: bad\ndescription: bad\n---\n")
		require.NoError(t, tw.WriteHeader(&tar.Header{Name: name, Typeflag: tar.TypeReg, Mode: 0o644, Size: int64(len(content))}))
		_, err = tw.Write(content)
		require.NoError(t, err)
	}
	require.NoError(t, tw.Close())
	require.NoError(t, file.Close())

	_, err = extractSkillTar(archivePath, filepath.Join(root, "out"), skillArchiveLimits{
		maxEntries:        1,
		maxEntryBytes:     maxSkillArchiveEntryBytes,
		maxExtractedBytes: maxSkillArchiveExtractedBytes,
	})
	require.ErrorIs(t, err, ErrInvalidArgument)
	require.Equal(t, SkillImportErrorArchiveLimitExceeded, Code(err))
	require.Contains(t, err.Error(), "too many entries")
}

func TestExtractSkillTarSkipsSymlinkEntries(t *testing.T) {
	root := t.TempDir()
	archivePath := filepath.Join(root, "unsafe.tar")
	file, err := os.Create(archivePath)
	require.NoError(t, err)
	tw := tar.NewWriter(file)
	require.NoError(t, tw.WriteHeader(&tar.Header{Name: "skill/link", Typeflag: tar.TypeSymlink, Linkname: "SKILL.md", Mode: 0o777}))
	content := []byte("---\nname: review-sk\ndescription: Review helper\n---\n")
	require.NoError(t, tw.WriteHeader(&tar.Header{Name: "skill/SKILL.md", Typeflag: tar.TypeReg, Mode: 0o644, Size: int64(len(content))}))
	_, err = tw.Write(content)
	require.NoError(t, err)
	require.NoError(t, tw.Close())
	require.NoError(t, file.Close())

	out := filepath.Join(root, "out")
	_, err = extractSkillSource(archivePath, "unsafe.tar", out)
	require.NoError(t, err)
	require.NoFileExists(t, filepath.Join(out, "skill", "link"))
	require.FileExists(t, filepath.Join(out, "skill", "SKILL.md"))
}

func writeSkill(t *testing.T, root, rel, name, description string) {
	t.Helper()
	writeFile(t, root, rel, "---\nname: "+name+"\ndescription: \""+description+"\"\n---\n\n# "+name+"\n")
}

func writeFile(t *testing.T, root, rel, content string) {
	t.Helper()
	path := filepath.Join(root, filepath.FromSlash(rel))
	require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
	require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
}

func writeGitTestFile(t *testing.T, filesystem billy.Filesystem, path, content string) {
	t.Helper()
	file, err := filesystem.Create(path)
	require.NoError(t, err)
	_, err = file.Write([]byte(content))
	require.NoError(t, err)
	require.NoError(t, file.Close())
}

func writeTarGz(t *testing.T, archivePath string, entries map[string]string) {
	t.Helper()
	file, err := os.Create(archivePath)
	require.NoError(t, err)
	defer file.Close()
	gz := gzip.NewWriter(file)
	defer gz.Close()
	tw := tar.NewWriter(gz)
	defer tw.Close()
	for name, content := range entries {
		require.NoError(t, tw.WriteHeader(&tar.Header{Name: name, Mode: 0o644, Size: int64(len(content))}))
		_, err := tw.Write([]byte(content))
		require.NoError(t, err)
	}
}
