package builtin

import (
	"context"
	"path/filepath"
	"sort"
	"strings"
	"testing"
)

func TestGrepGlobalExcludesFile(t *testing.T) {
	dir := mkRepo(t)
	writeFileT(t, filepath.Join(dir, "keep.txt"), "NEEDLE\n")
	writeFileT(t, filepath.Join(dir, "notes.tmp"), "NEEDLE\n")

	cfgDir := t.TempDir()
	excludes := filepath.Join(cfgDir, "global_ignore")
	writeFileT(t, excludes, "*.tmp\n")
	gitconfig := filepath.Join(cfgDir, "gitconfig")
	writeFileT(t, gitconfig, "[core]\n\texcludesFile = "+filepath.ToSlash(excludes)+"\n")
	t.Setenv("GIT_CONFIG_GLOBAL", gitconfig)

	out := runTool(t, grepTool{}, map[string]any{"pattern": "NEEDLE", "path": dir})
	if !strings.Contains(out, "keep.txt") {
		t.Fatalf("kept file must be found: %q", out)
	}
	if strings.Contains(out, "notes.tmp") {
		t.Fatalf("a file matched by core.excludesFile must be skipped: %q", out)
	}
}

// TestGrepWorkspaceE2E drives grep through the real per-workspace tool assembly
// (builtin.Workspace, what the desktop and ACP frontends use) against a repo that
// exercises every ignore rule at once: root + nested + /anchored .gitignore,
// nested negation, hidden dirs, and vendor dirs.
func TestGrepWorkspaceE2E(t *testing.T) {
	// Neutralize any machine-global git ignore so the fixture is hermetic.
	t.Setenv("GIT_CONFIG_GLOBAL", filepath.Join(t.TempDir(), "none"))
	t.Setenv("XDG_CONFIG_HOME", t.TempDir())

	repo := mkRepo(t)
	writeFileT(t, filepath.Join(repo, ".gitignore"), "*.log\nbuild/\n/dist/\n")
	writeFileT(t, filepath.Join(repo, "src", ".gitignore"), "generated.go\n")
	writeFileT(t, filepath.Join(repo, "pkg", ".gitignore"), "*.log\n!keep.log\n")

	keep := map[string]string{
		"src/app.go":   "NEEDLE app",
		"pkg/keep.log": "NEEDLE re-included",
		"README.md":    "NEEDLE readme",
	}
	drop := map[string]string{
		"src/generated.go":        "NEEDLE nested-ignored",
		"app.log":                 "NEEDLE root-log",
		"build/out.txt":           "NEEDLE ignored-dir",
		"dist/bundle.js":          "NEEDLE anchored-dir",
		"pkg/drop.log":            "NEEDLE nested-log",
		".github/ci.yml":          "NEEDLE hidden",
		"node_modules/x/index.js": "NEEDLE vendor",
	}
	for rel, body := range keep {
		writeFileT(t, filepath.Join(repo, filepath.FromSlash(rel)), body+"\n")
	}
	for rel, body := range drop {
		writeFileT(t, filepath.Join(repo, filepath.FromSlash(rel)), body+"\n")
	}

	grep := byName(Workspace{Dir: repo}.Tools())["grep"]
	out, err := grep.Execute(context.Background(), argsJSON(t, map[string]any{"pattern": "NEEDLE", "path": "."}))
	if err != nil {
		t.Fatal(err)
	}

	found := matchedRelFiles(repo, out)
	for rel := range keep {
		if !found[rel] {
			t.Errorf("expected %s in results, got %v", rel, sortedKeys(found))
		}
	}
	for rel := range drop {
		if found[rel] {
			t.Errorf("did not expect %s in results (got %v)", rel, sortedKeys(found))
		}
	}
}

// matchedRelFiles parses grep's "path:line:text" output into the set of matched
// files, relative to repo with forward slashes.
func matchedRelFiles(repo, out string) map[string]bool {
	found := map[string]bool{}
	for line := range strings.SplitSeq(out, "\n") {
		i := strings.LastIndex(line, ":")
		if i < 0 {
			continue
		}
		j := strings.LastIndex(line[:i], ":")
		if j < 0 {
			continue
		}
		if rel, err := filepath.Rel(repo, line[:j]); err == nil {
			found[filepath.ToSlash(rel)] = true
		}
	}
	return found
}

func sortedKeys(m map[string]bool) []string {
	var ks []string
	for k := range m {
		ks = append(ks, k)
	}
	sort.Strings(ks)
	return ks
}
