package skillsmanifest

import (
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestAllowedExposesAgentTeamInsightFileAnalyzerSkill(t *testing.T) {
	allowed, err := Allowed()
	require.NoError(t, err)

	ids := make([]string, 0, len(allowed))
	for _, spec := range allowed {
		ids = append(ids, spec.ID)
		require.NotEmpty(t, spec.SHA256)
		require.NotEmpty(t, spec.content)
		require.True(t, strings.HasPrefix(spec.RelativePath, AgentTeamInsightRootDir+"/"),
			"skill %s relativePath must live under %s/", spec.ID, AgentTeamInsightRootDir)
	}
	require.Contains(t, ids, "agentteam-insight.file-analyzer")
}

func TestMaterializeWritesAllowlistedSkillAndManifest(t *testing.T) {
	workDir := t.TempDir()
	manifest, err := Materialize(workDir)
	require.NoError(t, err)

	require.Equal(t, ManifestSchema, manifest.Schema)
	require.Equal(t, BundleVersion, manifest.BundleVersion)
	require.NotEmpty(t, manifest.GeneratedAt)

	allowed, err := Allowed()
	require.NoError(t, err)
	require.Len(t, manifest.Skills, len(allowed))

	skillsRoot := filepath.Join(workDir, SkillsRootDir)
	require.FileExists(t, filepath.Join(skillsRoot, manifestName))

	for _, spec := range allowed {
		path := filepath.Join(skillsRoot, filepath.FromSlash(spec.RelativePath))
		require.FileExists(t, path)
		data, err := os.ReadFile(path)
		require.NoError(t, err)
		require.Equal(t, spec.content, data)
	}
}

func TestMaterializeIsIdempotentWhenContentUnchanged(t *testing.T) {
	workDir := t.TempDir()
	first, err := Materialize(workDir)
	require.NoError(t, err)

	skillsRoot := filepath.Join(workDir, SkillsRootDir)
	manifestPath := filepath.Join(skillsRoot, manifestName)
	firstStat, err := os.Stat(manifestPath)
	require.NoError(t, err)

	second, err := Materialize(workDir)
	require.NoError(t, err)

	require.Equal(t, first.Skills, second.Skills)
	require.Equal(t, first.BundleVersion, second.BundleVersion)

	secondStat, err := os.Stat(manifestPath)
	require.NoError(t, err)
	require.Equal(t, firstStat.ModTime().UnixNano(), secondStat.ModTime().UnixNano(),
		"manifest file must not be rewritten on idempotent re-materialize")
}

func TestMaterializePrunesStaleAgentTeamInsightSkillEntries(t *testing.T) {
	workDir := t.TempDir()
	skillsRoot := filepath.Join(workDir, SkillsRootDir)
	insightRoot := filepath.Join(skillsRoot, AgentTeamInsightRootDir)

	staleDir := filepath.Join(insightRoot, "stale-skill")
	require.NoError(t, os.MkdirAll(staleDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(staleDir, "SKILL.md"), []byte("# stale\n"), 0o644))

	otherSkillDir := filepath.Join(skillsRoot, "user-owned-skill")
	require.NoError(t, os.MkdirAll(otherSkillDir, 0o755))
	require.NoError(t, os.WriteFile(filepath.Join(otherSkillDir, "SKILL.md"), []byte("# user\n"), 0o644))

	_, err := Materialize(workDir)
	require.NoError(t, err)

	require.NoDirExists(t, staleDir, "stale agentteam-insight skill must be pruned")
	require.DirExists(t, otherSkillDir, "user-owned skills outside agentteam-insight/ must be preserved")
}

func TestMaterializeRefreshesOutOfDateSkillContent(t *testing.T) {
	workDir := t.TempDir()
	skillsRoot := filepath.Join(workDir, SkillsRootDir)

	allowed, err := Allowed()
	require.NoError(t, err)
	require.NotEmpty(t, allowed)
	target := filepath.Join(skillsRoot, filepath.FromSlash(allowed[0].RelativePath))
	require.NoError(t, os.MkdirAll(filepath.Dir(target), 0o755))
	require.NoError(t, os.WriteFile(target, []byte("# stale content\n"), 0o644))

	_, err = Materialize(workDir)
	require.NoError(t, err)

	data, err := os.ReadFile(target)
	require.NoError(t, err)
	require.Equal(t, allowed[0].content, data, "stale content must be overwritten with allowlisted bytes")
}

func TestMatchesAllowedDistinguishesFreshFromStaleWorkDirs(t *testing.T) {
	workDir := t.TempDir()
	ok, err := MatchesAllowed(workDir)
	require.NoError(t, err)
	require.False(t, ok)

	_, err = Materialize(workDir)
	require.NoError(t, err)

	ok, err = MatchesAllowed(workDir)
	require.NoError(t, err)
	require.True(t, ok)
}

func TestConcurrentMaterializeAcrossIsolatedWorkDirsDoesNotCollision(t *testing.T) {
	allowed, err := Allowed()
	require.NoError(t, err)
	require.NotEmpty(t, allowed)

	const concurrency = 8
	type runResult struct {
		workDir   string
		manifest  *Manifest
		err       error
	}
	results := make(chan runResult, concurrency)
	for i := 0; i < concurrency; i++ {
		go func() {
			workDir := t.TempDir()
			manifest, err := Materialize(workDir)
			results <- runResult{workDir: workDir, manifest: manifest, err: err}
		}()
	}

	uniqueRoots := map[string]struct{}{}
	runs := make([]runResult, 0, concurrency)
	for i := 0; i < concurrency; i++ {
		got := <-results
		require.NoError(t, got.err)
		runs = append(runs, got)
		uniqueRoots[got.workDir] = struct{}{}
	}
	require.Len(t, uniqueRoots, concurrency,
		"each concurrent goroutine must materialize into its own workDir")

	expectedIDs := make([]string, 0, len(allowed))
	for _, spec := range allowed {
		expectedIDs = append(expectedIDs, spec.ID)
	}
	sort.Strings(expectedIDs)

	for _, run := range runs {
		require.Equal(t, ManifestSchema, run.manifest.Schema)
		require.Equal(t, BundleVersion, run.manifest.BundleVersion)

		gotIDs := make([]string, 0, len(run.manifest.Skills))
		for _, skill := range run.manifest.Skills {
			gotIDs = append(gotIDs, skill.ID)
		}
		sort.Strings(gotIDs)
		require.Equal(t, expectedIDs, gotIDs,
			"each isolated workDir must materialize the full allowlist")

		for _, spec := range allowed {
			data, err := os.ReadFile(filepath.Join(run.workDir, SkillsRootDir, filepath.FromSlash(spec.RelativePath)))
			require.NoError(t, err)
			require.Equal(t, spec.content, data,
				"concurrent runs must not bleed content across workDirs for skill %s", spec.ID)
		}

		ok, err := MatchesAllowed(run.workDir)
		require.NoError(t, err)
		require.True(t, ok, "each isolated workDir must independently satisfy MatchesAllowed")
	}
}
