package skill

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

// IsValidName

func TestIsValidName(t *testing.T) {
	cases := []struct {
		name string
		want bool
	}{
		{"valid-name", true},
		{"CamelCase", true},
		{"with.dot", true},
		{"with_underscore", true},
		{"a", true},
		{"A123", true},
		{"", false},
		{"-starts-dash", false},
		{"has space", false},
		{"has/slash", false},
		{strings.Repeat("a", 65), false}, // too long
		{strings.Repeat("a", 64), true},  // max length
	}
	for _, c := range cases {
		if got := IsValidName(c.name); got != c.want {
			t.Errorf("IsValidName(%q) = %v, want %v", c.name, got, c.want)
		}
	}
}

// splitFrontmatter

func TestSplitFrontmatterNoFence(t *testing.T) {
	fm, body := splitFrontmatter("just body")
	if len(fm) != 0 {
		t.Errorf("expected empty fm, got %v", fm)
	}
	if body != "just body" {
		t.Errorf("body = %q", body)
	}
}

func TestSplitFrontmatterUnclosed(t *testing.T) {
	fm, body := splitFrontmatter("---\nkey: val\n\nno closing")
	if len(fm) != 0 {
		t.Errorf("unclosed fence should return empty fm, got %v", fm)
	}
	if !strings.Contains(body, "---") {
		t.Errorf("body should contain original: %q", body)
	}
}

func TestSplitFrontmatterEmpty(t *testing.T) {
	fm, body := splitFrontmatter("")
	if len(fm) != 0 {
		t.Errorf("empty input should return empty fm, got %v", fm)
	}
	if body != "" {
		t.Errorf("body = %q", body)
	}
}

func TestSplitFrontmatterQuotedValues(t *testing.T) {
	fm, _ := splitFrontmatter("---\ndescription: \"quoted\"\n---\n")
	if fm["description"] != "quoted" {
		t.Errorf("description = %q", fm["description"])
	}
}

// parseAllowedTools

func TestParseAllowedToolsEmpty(t *testing.T) {
	if got := parseAllowedTools(""); got != nil {
		t.Errorf("empty = %v, want nil", got)
	}
	if got := parseAllowedTools("   "); got != nil {
		t.Errorf("whitespace = %v, want nil", got)
	}
}

func TestParseAllowedToolsSingle(t *testing.T) {
	got := parseAllowedTools("bash")
	if len(got) != 1 || got[0] != "bash" {
		t.Errorf("single = %v", got)
	}
}

func TestParseAllowedToolsMultiple(t *testing.T) {
	got := parseAllowedTools("read_file, grep, bash")
	if len(got) != 3 {
		t.Errorf("count = %d, want 3", len(got))
	}
	if got[0] != "read_file" || got[1] != "grep" || got[2] != "bash" {
		t.Errorf("tools = %v", got)
	}
}

func TestParseAllowedToolsTrailingComma(t *testing.T) {
	got := parseAllowedTools("bash,")
	if len(got) != 1 || got[0] != "bash" {
		t.Errorf("trailing comma = %v", got)
	}
}

func TestParseAllowedToolsExtraSpaces(t *testing.T) {
	got := parseAllowedTools("  bash  ,  grep  ")
	if len(got) != 2 || got[0] != "bash" || got[1] != "grep" {
		t.Errorf("extra spaces = %v", got)
	}
}

// parseRunAs

func TestParseRunAsExplicit(t *testing.T) {
	if parseRunAs("subagent", "", "") != RunSubagent {
		t.Error("explicit subagent should return RunSubagent")
	}
	if parseRunAs("inline", "", "") != RunInline {
		t.Error("explicit inline should return RunInline")
	}
}

func TestParseRunAsContextFork(t *testing.T) {
	if parseRunAs("", "fork", "") != RunSubagent {
		t.Error("context: fork should return RunSubagent")
	}
	if parseRunAs("", "FORK", "") != RunSubagent {
		t.Error("context: FORK should return RunSubagent")
	}
}

func TestParseRunAsAgent(t *testing.T) {
	if parseRunAs("", "", "some-agent") != RunSubagent {
		t.Error("non-empty agent should return RunSubagent")
	}
}

func TestParseRunAsDefault(t *testing.T) {
	if parseRunAs("", "", "") != RunInline {
		t.Error("all empty should default to RunInline")
	}
	if parseRunAs("unknown", "", "") != RunInline {
		t.Error("unknown runAs should default to RunInline")
	}
}

// resolveCustomPaths

func TestResolveCustomPathsTilde(t *testing.T) {
	home := t.TempDir()
	got := resolveCustomPaths([]string{"~/skills"}, "/base", home)
	if len(got) != 1 || got[0] != filepath.Join(home, "skills") {
		t.Errorf("tilde expansion = %v", got)
	}
}

func TestResolveCustomPathsRelative(t *testing.T) {
	base := t.TempDir()
	got := resolveCustomPaths([]string{"./my-skills"}, base, "/home")
	if len(got) != 1 || got[0] != filepath.Join(base, "my-skills") {
		t.Errorf("relative = %v", got)
	}
}

func TestResolveCustomPathsAbsolute(t *testing.T) {
	abs := filepath.Join(t.TempDir(), "absolute", "path")
	got := resolveCustomPaths([]string{abs}, "/base", "/home")
	if len(got) != 1 || got[0] != abs {
		t.Errorf("absolute = %v", got)
	}
}

func TestResolveCustomPathsEmpty(t *testing.T) {
	got := resolveCustomPaths([]string{"", "  "}, "/base", "/home")
	if len(got) != 0 {
		t.Errorf("empty paths should be filtered, got %v", got)
	}
}

// dedupePaths

func TestDedupePaths(t *testing.T) {
	got := dedupePaths([]string{"/a", "/b", "/a", "/c", "/b"})
	if len(got) != 3 || got[0] != "/a" || got[1] != "/b" || got[2] != "/c" {
		t.Errorf("deduped = %v", got)
	}
}

func TestDedupePathsEmpty(t *testing.T) {
	got := dedupePaths(nil)
	if len(got) != 0 {
		t.Errorf("nil = %v", got)
	}
}

// stubBody

func TestStubBody(t *testing.T) {
	body := stubBody("my-skill")
	if !strings.Contains(body, "name: my-skill") {
		t.Error("stub should contain the skill name")
	}
	if !strings.Contains(body, "description:") {
		t.Error("stub should contain description field")
	}
	if !strings.Contains(body, "# my-skill") {
		t.Error("stub should contain the skill name as heading")
	}
}

// Read edge cases

func TestReadInvalidName(t *testing.T) {
	home := t.TempDir()
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	_, ok := st.Read("invalid name!")
	if ok {
		t.Error("invalid name should return ok=false")
	}
}

func TestReadNotFound(t *testing.T) {
	home := t.TempDir()
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	_, ok := st.Read("nonexistent")
	if ok {
		t.Error("nonexistent skill should return ok=false")
	}
}

// Create edge cases

func TestCreateInvalidName(t *testing.T) {
	home := t.TempDir()
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	_, err := st.Create("invalid name!", ScopeGlobal)
	if err == nil {
		t.Error("invalid name should error")
	}
}

func TestCreateProjectScopeRequiresRoot(t *testing.T) {
	home := t.TempDir()
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	_, err := st.Create("test", ScopeProject)
	if err == nil {
		t.Error("project scope without root should error")
	}
}

func TestCreateDirectoryLayoutSkill(t *testing.T) {
	home := t.TempDir()
	skillsRoot := filepath.Join(home, ".reasonix", "skills", "existing", "SKILL.md")
	os.MkdirAll(filepath.Dir(skillsRoot), 0o755)
	os.WriteFile(skillsRoot, []byte("---\ndescription: exists\n---\nbody"), 0o644)
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	_, err := st.Create("existing", ScopeGlobal)
	if err == nil {
		t.Error("should refuse to overwrite directory-layout skill")
	}
}

func TestUpdateContentOverwritesExistingSkill(t *testing.T) {
	home := t.TempDir()
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	if _, err := st.CreateWithContent("editable", ScopeGlobal, "---\ndescription: v1\n---\nold body"); err != nil {
		t.Fatalf("CreateWithContent: %v", err)
	}
	if err := st.UpdateContent("editable", ScopeGlobal, "---\ndescription: v2\n---\nnew body"); err != nil {
		t.Fatalf("UpdateContent: %v", err)
	}
	sk, ok := st.Read("editable")
	if !ok {
		t.Fatal("skill missing after update")
	}
	if sk.Description != "v2" || sk.Body != "new body" {
		t.Fatalf("update did not apply: description=%q body=%q", sk.Description, sk.Body)
	}
}

func TestUpdateContentRefusesBuiltin(t *testing.T) {
	st := New(Options{HomeDir: t.TempDir()})
	if err := st.UpdateContent("explore", ScopeBuiltin, "---\ndescription: x\n---\nbody"); err == nil {
		t.Error("updating a builtin should error")
	}
}

func TestUpdateContentRefusesMissingSkill(t *testing.T) {
	st := New(Options{HomeDir: t.TempDir(), DisableBuiltins: true})
	if err := st.UpdateContent("does-not-exist", ScopeGlobal, "---\ndescription: x\n---\nbody"); err == nil {
		t.Error("updating a nonexistent skill should error")
	}
}

func TestUpdateContentRefusesScopeMismatch(t *testing.T) {
	home := t.TempDir()
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	if _, err := st.CreateWithContent("scoped2", ScopeGlobal, "---\ndescription: v1\n---\nbody"); err != nil {
		t.Fatalf("CreateWithContent: %v", err)
	}
	if err := st.UpdateContent("scoped2", ScopeProject, "---\ndescription: v2\n---\nbody"); err == nil {
		t.Error("updating with the wrong scope should error")
	}
	sk, ok := st.Read("scoped2")
	if !ok || sk.Description != "v1" {
		t.Fatalf("skill should be unchanged after a refused scope-mismatched update, got description=%q ok=%v", sk.Description, ok)
	}
}

func TestUpdateContentRefusesSymlinkedFlatSkill(t *testing.T) {
	home := t.TempDir()
	outside := filepath.Join(t.TempDir(), "outside.md")
	original := "---\ndescription: outside\nrunAs: subagent\ninvocation: manual\n---\noriginal"
	if err := os.WriteFile(outside, []byte(original), 0o644); err != nil {
		t.Fatal(err)
	}
	root := filepath.Join(home, ".reasonix", SkillsDirname)
	if err := os.MkdirAll(root, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.Symlink(outside, filepath.Join(root, "linked.md")); err != nil {
		t.Skipf("symlinks unavailable: %v", err)
	}
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	if _, ok := st.Read("linked"); !ok {
		t.Fatal("symlinked flat skill should remain readable")
	}
	if err := st.UpdateContent("linked", ScopeGlobal, "changed"); err == nil {
		t.Fatal("updating a symlinked flat skill should fail")
	}
	got, err := os.ReadFile(outside)
	if err != nil || string(got) != original {
		t.Fatalf("outside target changed: content=%q err=%v", got, err)
	}
}

func TestUpdateContentRefusesSymlinkedDirectorySkill(t *testing.T) {
	home := t.TempDir()
	outsideDir := t.TempDir()
	outside := filepath.Join(outsideDir, SkillFile)
	original := "---\ndescription: outside\nrunAs: subagent\ninvocation: manual\n---\noriginal"
	if err := os.WriteFile(outside, []byte(original), 0o644); err != nil {
		t.Fatal(err)
	}
	root := filepath.Join(home, ".reasonix", SkillsDirname)
	if err := os.MkdirAll(root, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.Symlink(outsideDir, filepath.Join(root, "linked-dir")); err != nil {
		t.Skipf("symlinks unavailable: %v", err)
	}
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	if _, ok := st.Read("linked-dir"); !ok {
		t.Fatal("symlinked directory skill should remain readable")
	}
	if err := st.UpdateContent("linked-dir", ScopeGlobal, "changed"); err == nil {
		t.Fatal("updating a symlinked directory skill should fail")
	}
	got, err := os.ReadFile(outside)
	if err != nil || string(got) != original {
		t.Fatalf("outside target changed: content=%q err=%v", got, err)
	}
}

func TestDeleteSymlinkedSkillsRemovesLinksNotTargets(t *testing.T) {
	home := t.TempDir()
	root := filepath.Join(home, ".reasonix", SkillsDirname)
	if err := os.MkdirAll(root, 0o755); err != nil {
		t.Fatal(err)
	}
	outsideDir := t.TempDir()
	flatTarget := filepath.Join(outsideDir, "flat-target.md")
	dirTarget := filepath.Join(outsideDir, "directory-target")
	if err := os.MkdirAll(dirTarget, 0o755); err != nil {
		t.Fatal(err)
	}
	content := []byte("---\ndescription: linked\nrunAs: subagent\ninvocation: manual\n---\nbody")
	if err := os.WriteFile(flatTarget, content, 0o644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dirTarget, SkillFile), content, 0o644); err != nil {
		t.Fatal(err)
	}
	flatLink := filepath.Join(root, "flat-link.md")
	dirLink := filepath.Join(root, "dir-link")
	if err := os.Symlink(flatTarget, flatLink); err != nil {
		t.Skipf("symlinks unavailable: %v", err)
	}
	if err := os.Symlink(dirTarget, dirLink); err != nil {
		t.Skipf("symlinks unavailable: %v", err)
	}
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	for _, name := range []string{"flat-link", "dir-link"} {
		if err := st.Delete(name, ScopeGlobal); err != nil {
			t.Fatalf("Delete(%q): %v", name, err)
		}
	}
	if _, err := os.Lstat(flatLink); !os.IsNotExist(err) {
		t.Fatalf("flat link still exists: %v", err)
	}
	if _, err := os.Lstat(dirLink); !os.IsNotExist(err) {
		t.Fatalf("directory link still exists: %v", err)
	}
	if got, err := os.ReadFile(flatTarget); err != nil || string(got) != string(content) {
		t.Fatalf("flat target changed: content=%q err=%v", got, err)
	}
	if got, err := os.ReadFile(filepath.Join(dirTarget, SkillFile)); err != nil || string(got) != string(content) {
		t.Fatalf("directory target changed: content=%q err=%v", got, err)
	}
}

func TestDeleteRemovesDirectoryLayoutSkill(t *testing.T) {
	home := t.TempDir()
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	path, err := st.CreateWithContent("throwaway", ScopeGlobal, "---\ndescription: x\n---\nbody")
	if err != nil {
		t.Fatalf("CreateWithContent: %v", err)
	}
	if err := st.Delete("throwaway", ScopeGlobal); err != nil {
		t.Fatalf("Delete: %v", err)
	}
	if _, ok := st.Read("throwaway"); ok {
		t.Fatal("skill should be gone after Delete")
	}
	if _, err := os.Stat(filepath.Dir(path)); !os.IsNotExist(err) {
		t.Fatalf("skill directory should be removed, stat err=%v", err)
	}
}

func TestDeleteRefusesBuiltin(t *testing.T) {
	st := New(Options{HomeDir: t.TempDir()})
	if err := st.Delete("explore", ScopeBuiltin); err == nil {
		t.Error("deleting a builtin should error")
	}
}

func TestDeleteRefusesMissingSkill(t *testing.T) {
	st := New(Options{HomeDir: t.TempDir(), DisableBuiltins: true})
	if err := st.Delete("does-not-exist", ScopeGlobal); err == nil {
		t.Error("deleting a nonexistent skill should error")
	}
}

func TestDeleteRefusesScopeMismatch(t *testing.T) {
	home := t.TempDir()
	st := New(Options{HomeDir: home, DisableBuiltins: true})
	if _, err := st.CreateWithContent("scoped", ScopeGlobal, "---\ndescription: x\n---\nbody"); err != nil {
		t.Fatalf("CreateWithContent: %v", err)
	}
	// The skill actually lives at ScopeGlobal; a ScopeProject delete request
	// for the same name must refuse rather than silently no-op or, worse,
	// resolve to an unrelated file.
	if err := st.Delete("scoped", ScopeProject); err == nil {
		t.Error("deleting with the wrong scope should error")
	}
	if _, ok := st.Read("scoped"); !ok {
		t.Fatal("skill should survive a refused scope-mismatched delete")
	}
}

// New edge cases

func TestNewWithCustomPaths(t *testing.T) {
	custom := t.TempDir()
	st := New(Options{HomeDir: t.TempDir(), CustomPaths: []string{custom}, DisableBuiltins: true})
	roots := st.Roots()
	found := false
	for _, r := range roots {
		if r.Dir == custom && r.Scope == ScopeCustom {
			found = true
			break
		}
	}
	if !found {
		t.Error("custom path not in roots")
	}
}

func TestHasProjectScope(t *testing.T) {
	st1 := New(Options{HomeDir: t.TempDir(), ProjectRoot: "/some/project"})
	if !st1.HasProjectScope() {
		t.Error("with project root should return true")
	}
	st2 := New(Options{HomeDir: t.TempDir()})
	if st2.HasProjectScope() {
		t.Error("without project root should return false")
	}
}
