package mcp

import (
	"os"
	"os/exec"
	"path/filepath"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/zap"

	"github.com/zzet/gortex/internal/config"
	"github.com/zzet/gortex/internal/graph"
	"github.com/zzet/gortex/internal/indexer"
	"github.com/zzet/gortex/internal/parser"
	"github.com/zzet/gortex/internal/parser/languages"
	"github.com/zzet/gortex/internal/query"
	"github.com/zzet/gortex/internal/search"
)

// gitInit runs git in dir, failing the test on error. Kept local so the
// fileops worktree test doesn't depend on a helper from another package.
func gitInit(t *testing.T, dir string, args ...string) {
	t.Helper()
	cmd := exec.Command("git", args...)
	cmd.Dir = dir
	cmd.Env = append(os.Environ(),
		"GIT_CONFIG_GLOBAL=/dev/null",
		"GIT_CONFIG_SYSTEM=/dev/null",
	)
	out, err := cmd.CombinedOutput()
	require.NoErrorf(t, err, "git %v: %s", args, string(out))
}

// resolvePath evaluates symlinks so a macOS /var -> /private/var alias
// can't break a filesystem-path equality assertion.
func resolvePath(t *testing.T, p string) string {
	t.Helper()
	if r, err := filepath.EvalSymlinks(p); err == nil {
		return r
	}
	return p
}

// setupWorktreePair builds a real git repo with one linked worktree.
// The main checkout and the worktree each hold the same repo-relative
// file (shared.go) plus their own file. Returns the main checkout path,
// the worktree path, and a Server whose MultiIndexer tracks both.
func setupWorktreePair(t *testing.T) (mainRepo, worktree string, srv *Server) {
	t.Helper()
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git binary not available in PATH")
	}

	mainRepo = filepath.Join(t.TempDir(), "main-checkout")
	require.NoError(t, os.MkdirAll(mainRepo, 0o755))
	gitInit(t, mainRepo, "init", "-q", "-b", "main")
	gitInit(t, mainRepo, "config", "user.email", "test@example.com")
	gitInit(t, mainRepo, "config", "user.name", "Test")
	gitInit(t, mainRepo, "config", "commit.gpgsign", "false")
	require.NoError(t, os.WriteFile(filepath.Join(mainRepo, "shared.go"),
		[]byte("package main\n\nfunc Shared() string { return \"main\" }\n"), 0o644))
	gitInit(t, mainRepo, "add", ".")
	gitInit(t, mainRepo, "commit", "-q", "-m", "init")

	// A linked worktree on its own branch. `git worktree add` checks
	// out shared.go into it; the test then gives the worktree its own
	// distinct copy so a misrooted write is unambiguous.
	worktree = filepath.Join(t.TempDir(), "feature-worktree")
	gitInit(t, mainRepo, "worktree", "add", "-q", "-b", "feature", worktree)
	require.NoError(t, os.WriteFile(filepath.Join(worktree, "shared.go"),
		[]byte("package main\n\nfunc Shared() string { return \"worktree\" }\n"), 0o644))

	cfgPath := filepath.Join(t.TempDir(), "config.yaml")
	gc := &config.GlobalConfig{Repos: []config.RepoEntry{
		{Path: mainRepo, Name: "main-checkout"},
		{Path: worktree, Name: "feature-worktree"},
	}}
	gc.SetConfigPath(cfgPath)
	require.NoError(t, gc.Save())
	cm, err := config.NewConfigManager(cfgPath)
	require.NoError(t, err)

	reg := parser.NewRegistry()
	reg.Register(languages.NewGoExtractor())

	g := graph.New()
	mi := indexer.NewMultiIndexer(g, reg, search.NewBM25(), cm, zap.NewNop())
	_, err = mi.IndexAll()
	require.NoError(t, err)
	require.True(t, mi.IsMultiRepo())

	eng := query.NewEngine(g)
	srv = NewServer(eng, g, nil, nil, zap.NewNop(), nil, MultiRepoOptions{
		ConfigManager: cm,
		MultiIndexer:  mi,
	})
	return mainRepo, worktree, srv
}

// TestEditFile_LandsInLinkedWorktree is the core piece-2 scenario: an
// edit addressed to a file in a linked worktree must modify that
// worktree's copy on disk, not the main checkout's. Both checkouts
// share one repo identity for index-cache reuse, so the resolver has to
// re-root the write at the correct worktree.
func TestEditFile_LandsInLinkedWorktree(t *testing.T) {
	mainRepo, worktree, srv := setupWorktreePair(t)

	mainShared := filepath.Join(mainRepo, "shared.go")
	wtShared := filepath.Join(worktree, "shared.go")
	mainBefore, err := os.ReadFile(mainShared)
	require.NoError(t, err)

	// Edit shared.go via the worktree's repo prefix.
	result := callTool(t, srv, "edit_file", map[string]any{
		"path":       "feature-worktree/shared.go",
		"old_string": `return "worktree"`,
		"new_string": `return "worktree-edited"`,
	})
	assert.False(t, result.IsError, "edit of a worktree file must succeed: %+v", result.Content)
	resp := decodeFileOpsResult(t, result)
	assert.Equal(t, "applied", resp["status"])

	// The worktree's copy carries the edit.
	wtAfter, err := os.ReadFile(wtShared)
	require.NoError(t, err)
	assert.Contains(t, string(wtAfter), `return "worktree-edited"`,
		"the linked worktree's file must reflect the edit")

	// The main checkout's copy is untouched — the edit did NOT leak
	// into the main repo.
	mainAfter, err := os.ReadFile(mainShared)
	require.NoError(t, err)
	assert.Equal(t, string(mainBefore), string(mainAfter),
		"the main checkout's file must NOT be modified by a worktree-targeted edit")
}

// TestWriteFile_LandsInLinkedWorktree proves write_file (full-content
// overwrite) honours the same worktree rooting as edit_file.
func TestWriteFile_LandsInLinkedWorktree(t *testing.T) {
	mainRepo, worktree, srv := setupWorktreePair(t)

	mainShared := filepath.Join(mainRepo, "shared.go")
	mainBefore, err := os.ReadFile(mainShared)
	require.NoError(t, err)

	result := callTool(t, srv, "write_file", map[string]any{
		"path":    "feature-worktree/shared.go",
		"content": "package main\n\nfunc Shared() string { return \"rewritten-in-worktree\" }\n",
	})
	assert.False(t, result.IsError, "write to a worktree file must succeed: %+v", result.Content)

	wtAfter, err := os.ReadFile(filepath.Join(worktree, "shared.go"))
	require.NoError(t, err)
	assert.Contains(t, string(wtAfter), "rewritten-in-worktree",
		"write_file must land in the worktree's copy")

	mainAfter, err := os.ReadFile(mainShared)
	require.NoError(t, err)
	assert.Equal(t, string(mainBefore), string(mainAfter),
		"write_file targeting the worktree must not overwrite the main checkout")
}

// TestEditFile_MainCheckoutPrefixStillEditsMainCheckout is the other
// side of the rooting contract: an edit addressed via the *main*
// checkout's prefix must land in the main checkout, not bleed into a
// worktree. Worktree rooting must not hijack writes that legitimately
// target the main repo.
func TestEditFile_MainCheckoutPrefixStillEditsMainCheckout(t *testing.T) {
	mainRepo, worktree, srv := setupWorktreePair(t)

	wtShared := filepath.Join(worktree, "shared.go")
	wtBefore, err := os.ReadFile(wtShared)
	require.NoError(t, err)

	result := callTool(t, srv, "edit_file", map[string]any{
		"path":       "main-checkout/shared.go",
		"old_string": `return "main"`,
		"new_string": `return "main-edited"`,
	})
	assert.False(t, result.IsError, "edit of a main-checkout file must succeed: %+v", result.Content)

	mainAfter, err := os.ReadFile(filepath.Join(mainRepo, "shared.go"))
	require.NoError(t, err)
	assert.Contains(t, string(mainAfter), `return "main-edited"`,
		"the main checkout's file must reflect the edit")

	wtAfter, err := os.ReadFile(wtShared)
	require.NoError(t, err)
	assert.Equal(t, string(wtBefore), string(wtAfter),
		"the linked worktree's file must NOT be modified by a main-checkout edit")
}

// fakeWorktreeLookup is a multiRepoLookup stub for testing
// worktreeRootedPath without standing up a full MultiIndexer.
type fakeWorktreeLookup struct {
	worktrees map[string][]string // mainRepoPath -> linked worktree roots
}

func (f fakeWorktreeLookup) RepoPrefixes() []string                   { return nil }
func (f fakeWorktreeLookup) RepoRoot(string) (string, bool)           { return "", false }
func (f fakeWorktreeLookup) LinkedWorktreeRoots(main string) []string { return f.worktrees[main] }

// TestWorktreeRootedPath covers the re-rooting helper's decision table
// directly: it re-roots only when a file genuinely lives in a sibling
// worktree, and leaves the path alone in every conservative case.
func TestWorktreeRootedPath(t *testing.T) {
	if _, err := exec.LookPath("git"); err != nil {
		t.Skip("git binary not available in PATH")
	}

	// Real git repo + worktree so ResolveWorktree classifies correctly.
	mainRepo := filepath.Join(t.TempDir(), "main")
	require.NoError(t, os.MkdirAll(mainRepo, 0o755))
	gitInit(t, mainRepo, "init", "-q", "-b", "main")
	gitInit(t, mainRepo, "config", "user.email", "test@example.com")
	gitInit(t, mainRepo, "config", "user.name", "Test")
	gitInit(t, mainRepo, "config", "commit.gpgsign", "false")
	require.NoError(t, os.WriteFile(filepath.Join(mainRepo, "seed.go"),
		[]byte("package main\n"), 0o644))
	gitInit(t, mainRepo, "add", ".")
	gitInit(t, mainRepo, "commit", "-q", "-m", "init")

	worktree := filepath.Join(t.TempDir(), "wt")
	gitInit(t, mainRepo, "worktree", "add", "-q", "-b", "feature", worktree)

	// A file that exists ONLY in the worktree.
	require.NoError(t, os.WriteFile(filepath.Join(worktree, "only_wt.go"),
		[]byte("package main\n"), 0o644))
	// A file that exists in the main checkout.
	require.NoError(t, os.WriteFile(filepath.Join(mainRepo, "in_main.go"),
		[]byte("package main\n"), 0o644))

	// worktreeRootedPath passes the resolved root through to
	// LinkedWorktreeRoots verbatim, so the stub is keyed by the same
	// path string the helper will hand it.
	mi := fakeWorktreeLookup{worktrees: map[string][]string{
		mainRepo: {worktree},
		worktree: {worktree},
	}}

	t.Run("re-roots a file that lives only in the worktree", func(t *testing.T) {
		// Resolved under the main checkout, but the file is not there.
		abs := filepath.Join(mainRepo, "only_wt.go")
		got := worktreeRootedPath(abs, mainRepo, mi)
		assert.Equal(t, resolvePath(t, filepath.Join(worktree, "only_wt.go")),
			resolvePath(t, got),
			"a file present only in the worktree must be re-rooted there")
	})

	t.Run("leaves a file that exists in the resolved root", func(t *testing.T) {
		abs := filepath.Join(mainRepo, "in_main.go")
		got := worktreeRootedPath(abs, mainRepo, mi)
		assert.Equal(t, abs, got,
			"a file that exists under the resolved root must not be moved")
	})

	t.Run("leaves a brand-new file under the named prefix", func(t *testing.T) {
		// A file in no checkout — a fresh write_file. It must stay
		// where the caller addressed it.
		abs := filepath.Join(mainRepo, "brand_new.go")
		got := worktreeRootedPath(abs, mainRepo, mi)
		assert.Equal(t, abs, got,
			"a new file must land under the prefix the caller named")
	})

	t.Run("leaves a path already inside a worktree", func(t *testing.T) {
		// Root is the worktree itself — the file is already in the
		// right checkout, nothing to re-root.
		abs := filepath.Join(worktree, "only_wt.go")
		got := worktreeRootedPath(abs, worktree, mi)
		assert.Equal(t, abs, got,
			"a path resolved against a worktree root must be left untouched")
	})

	t.Run("nil lookup is a no-op", func(t *testing.T) {
		abs := filepath.Join(mainRepo, "only_wt.go")
		assert.Equal(t, abs, worktreeRootedPath(abs, mainRepo, nil))
	})
}
