package mcp

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	mcplib "github.com/mark3labs/mcp-go/mcp"
	"github.com/stretchr/testify/require"

	"github.com/zzet/gortex/internal/daemon"
)

// jsonUnmarshal is a test-local thin wrapper that hides the import
// from the per-call sites and keeps file-top imports tidy.
func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) }

// branchBootstrap is the minimal setup for branch tests: an overlay
// server with a single registered MCP session and a context wired
// with the session ID. The returned helpers wrap the verbose
// callToolByName + JSON-text plumbing into a tighter, intention-
// revealing surface.
func branchBootstrap(t *testing.T) (srv *Server, sessID, dir, targetFile, callerFile string, ctx context.Context) {
	t.Helper()
	srv, dir, targetFile, callerFile = setupOverlayServer(t)
	sessID = fmt.Sprintf("branch-test-%d", time.Now().UnixNano())
	require.NoError(t, srv.OverlayManager().RegisterWithID(sessID, ""))
	ctx = WithSessionID(context.Background(), sessID)
	return srv, sessID, dir, targetFile, callerFile, ctx
}

// invokeTool is the cousin of callToolByName that returns both the
// raw body and the textual content joined into one string — exactly
// what every branch test wants for substring assertions.
func invokeTool(t *testing.T, srv *Server, ctx context.Context, name string, args map[string]any) (*mcplib.CallToolResult, string) {
	t.Helper()
	res := callToolByName(t, srv, ctx, name, args)
	return res, toolText(res)
}

// TestOverlayBranch_ForkPreservesFiles is requirement 1: pushing a
// file on `main`, then forking, leaves both branches seeing the
// file. The fork must deep-copy so subsequent edits don't bleed
// across branches.
func TestOverlayBranch_ForkPreservesFiles(t *testing.T) {
	srv, sessID, _, targetFile, _, ctx := branchBootstrap(t)

	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    targetFile,
		Content: "package main\n\nfunc Target() {}\n\nfunc MainBranchSentinel() {}\n",
	}, nil))

	_, forkText := invokeTool(t, srv, ctx, "overlay_fork", map[string]any{
		"name": "strategy-a",
	})
	require.Contains(t, forkText, `"branch":"strategy-a"`)
	require.Contains(t, forkText, `"parent":"main"`)
	require.Contains(t, forkText, `"files":1`)

	// strategy-a sees the file (post-switch).
	require.NoError(t, srv.OverlayManager().SwitchBranch(sessID, "strategy-a"))
	files, err := srv.OverlayManager().Files(sessID)
	require.NoError(t, err)
	require.Contains(t, files, targetFile, "strategy-a must inherit main's file map")

	// main still sees the file (post-switch back).
	require.NoError(t, srv.OverlayManager().SwitchBranch(sessID, "main"))
	files, err = srv.OverlayManager().Files(sessID)
	require.NoError(t, err)
	require.Contains(t, files, targetFile, "main must keep its file after the fork")
}

// TestOverlayBranch_BranchesDiverge is requirement 2: after forking
// main→"a" and editing the same path on "a", main's content stays
// unchanged. This is the load-bearing isolation invariant that
// makes branching useful.
func TestOverlayBranch_BranchesDiverge(t *testing.T) {
	srv, sessID, _, targetFile, _, ctx := branchBootstrap(t)

	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    targetFile,
		Content: "package main\n\nfunc Target() {}\n\nfunc OnMainOnly() {}\n",
	}, nil))

	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{
		"name":     "a",
		"activate": true,
	})

	// On branch a, push a different content for the same path.
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    targetFile,
		Content: "package main\n\nfunc Target() {}\n\nfunc OnBranchAOnly() {}\n",
	}, nil))

	// Switch back to main and confirm content unchanged.
	_, _ = invokeTool(t, srv, ctx, "overlay_switch", map[string]any{"name": "main"})
	files, err := srv.OverlayManager().Files(sessID)
	require.NoError(t, err)
	require.Contains(t, files[targetFile].Content, "OnMainOnly")
	require.NotContains(t, files[targetFile].Content, "OnBranchAOnly")

	// And on a the content is the new one.
	_, _ = invokeTool(t, srv, ctx, "overlay_switch", map[string]any{"name": "a"})
	files, err = srv.OverlayManager().Files(sessID)
	require.NoError(t, err)
	require.Contains(t, files[targetFile].Content, "OnBranchAOnly")
	require.NotContains(t, files[targetFile].Content, "OnMainOnly")
}

// TestOverlayBranch_List is requirement 3: three forks yield four
// branches (main + 3) and the active flag is correct.
func TestOverlayBranch_List(t *testing.T) {
	srv, _, _, _, _, ctx := branchBootstrap(t)
	for _, name := range []string{"alpha", "beta", "gamma"} {
		_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": name})
	}
	_, listText := invokeTool(t, srv, ctx, "overlay_branches", map[string]any{})
	for _, name := range []string{"main", "alpha", "beta", "gamma"} {
		require.Contains(t, listText, fmt.Sprintf(`"name":%q`, name), "branch %s should appear in the list", name)
	}
	require.Contains(t, listText, `"count":4`)
	// Active flag: only main is active here (none of the forks
	// passed activate:true). The active=true entry must also be
	// the main entry — verify by checking the active-row JSON.
	require.True(t, listForActiveRowMatches(listText, "main"),
		"expected main to be the active branch row: %s", listText)
}

// TestOverlayBranch_Switch is requirement 4: switching changes the
// active branch and subsequent queries reflect that branch.
func TestOverlayBranch_Switch(t *testing.T) {
	srv, sessID, _, targetFile, _, ctx := branchBootstrap(t)

	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "a"})
	require.NoError(t, srv.OverlayManager().SwitchBranch(sessID, "a"))
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    targetFile,
		Content: "package main\n\nfunc Target() {}\n\nfunc AOnly() {}\n",
	}, nil))

	// Switch via the tool (not just the manager) and confirm a
	// subsequent get_file_summary sees the a-only symbol.
	_, swText := invokeTool(t, srv, ctx, "overlay_switch", map[string]any{"name": "a"})
	require.Contains(t, swText, `"active_branch":"a"`)

	_, summary := invokeTool(t, srv, ctx, "get_file_summary", map[string]any{
		"path": filepath.Base(targetFile),
	})
	require.Contains(t, summary, "AOnly", "after switch, active branch buffer must drive the view")

	// Switch back to main: AOnly disappears.
	_, _ = invokeTool(t, srv, ctx, "overlay_switch", map[string]any{"name": "main"})
	_, summary = invokeTool(t, srv, ctx, "get_file_summary", map[string]any{
		"path": filepath.Base(targetFile),
	})
	require.NotContains(t, summary, "AOnly", "main should not see branch-a-only symbol")
}

// TestOverlayBranch_Drop is requirement 5: drop removes the branch
// from the list. Cannot drop the active branch.
func TestOverlayBranch_Drop(t *testing.T) {
	srv, _, _, _, _, ctx := branchBootstrap(t)
	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "throwaway"})

	_, dropText := invokeTool(t, srv, ctx, "overlay_drop_branch", map[string]any{"name": "throwaway"})
	require.Contains(t, dropText, `"ok":true`)

	_, listText := invokeTool(t, srv, ctx, "overlay_branches", map[string]any{})
	require.NotContains(t, listText, `"name":"throwaway"`)

	// Cannot drop active: fork a, switch to it, then try to drop it.
	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "active-victim", "activate": true})
	res, dropAct := invokeTool(t, srv, ctx, "overlay_drop_branch", map[string]any{"name": "active-victim"})
	require.True(t, res.IsError)
	require.Contains(t, dropAct, "active overlay branch")
}

// TestOverlayBranch_CannotDropMain is requirement 6: refuse to drop
// the implicit main branch.
func TestOverlayBranch_CannotDropMain(t *testing.T) {
	srv, _, _, _, _, ctx := branchBootstrap(t)
	res, body := invokeTool(t, srv, ctx, "overlay_drop_branch", map[string]any{"name": "main"})
	require.True(t, res.IsError)
	require.Contains(t, body, "main overlay branch")
}

// TestOverlayBranch_Merge_NonConflicting is requirement 7: branches
// touching different files merge cleanly; the destination gains
// both file sets.
func TestOverlayBranch_Merge_NonConflicting(t *testing.T) {
	srv, sessID, dir, _, _, ctx := branchBootstrap(t)
	fileA := filepath.Join(dir, "a_only.go")
	fileB := filepath.Join(dir, "b_only.go")
	// Both files must exist on disk so the overlay parser can
	// resolve them — empty .go files are valid for the parser.
	require.NoError(t, os.WriteFile(fileA, []byte("package main\n"), 0o644))
	require.NoError(t, os.WriteFile(fileB, []byte("package main\n"), 0o644))

	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "a", "activate": true})
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    fileA,
		Content: "package main\n\nfunc OnA() {}\n",
	}, nil))

	_, _ = invokeTool(t, srv, ctx, "overlay_switch", map[string]any{"name": "main"})
	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "b", "activate": true})
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    fileB,
		Content: "package main\n\nfunc OnB() {}\n",
	}, nil))

	_, mergeText := invokeTool(t, srv, ctx, "overlay_merge", map[string]any{
		"from": "a",
		"into": "b",
	})
	require.Contains(t, mergeText, `"merged":1`)

	// b now has both files.
	require.NoError(t, srv.OverlayManager().SwitchBranch(sessID, "b"))
	files, err := srv.OverlayManager().Files(sessID)
	require.NoError(t, err)
	require.Contains(t, files, fileA, "merge should give b a copy of a's files")
	require.Contains(t, files, fileB, "b must keep its own file")
}

// TestOverlayBranch_Merge_Conflict is requirement 8: same-path
// different-content collisions block the merge without force and
// the response carries the conflict list; with force the merge
// proceeds last-writer-wins.
func TestOverlayBranch_Merge_Conflict(t *testing.T) {
	srv, sessID, _, targetFile, _, ctx := branchBootstrap(t)

	// Set up two branches that both edit targetFile differently.
	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "a", "activate": true})
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    targetFile,
		Content: "package main\n\nfunc Target() {}\n\nfunc FromA() {}\n",
	}, nil))

	_, _ = invokeTool(t, srv, ctx, "overlay_switch", map[string]any{"name": "main"})
	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "b", "activate": true})
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    targetFile,
		Content: "package main\n\nfunc Target() {}\n\nfunc FromB() {}\n",
	}, nil))

	// Without force: aborts with conflicts list.
	_, conflictText := invokeTool(t, srv, ctx, "overlay_merge", map[string]any{
		"from": "a",
		"into": "b",
	})
	require.Contains(t, conflictText, "merge conflict")
	require.Contains(t, conflictText, `"conflicts":["`+jsonEscape(targetFile)+`"]`)

	// Confirm b's content is unchanged.
	require.NoError(t, srv.OverlayManager().SwitchBranch(sessID, "b"))
	files, err := srv.OverlayManager().Files(sessID)
	require.NoError(t, err)
	require.Contains(t, files[targetFile].Content, "FromB", "non-forced merge must leave destination untouched")
	require.NotContains(t, files[targetFile].Content, "FromA")

	// With force: a wins (last-writer-wins).
	_, forceText := invokeTool(t, srv, ctx, "overlay_merge", map[string]any{
		"from":  "a",
		"into":  "b",
		"force": true,
	})
	require.Contains(t, forceText, `"merged":1`)
	require.Contains(t, forceText, `"resolution":"last-writer-wins"`)

	files, err = srv.OverlayManager().Files(sessID)
	require.NoError(t, err)
	require.Contains(t, files[targetFile].Content, "FromA", "force merge must overwrite destination with source")
}

// TestOverlayBranch_MergeToDisk is requirement 9: merging a branch
// with to_disk:true writes the file to disk and the on-disk
// content matches.
func TestOverlayBranch_MergeToDisk(t *testing.T) {
	srv, sessID, _, targetFile, _, ctx := branchBootstrap(t)

	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "writer", "activate": true})
	newContent := "package main\n\nfunc Target() {}\n\nfunc OnDisk() {}\n"
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    targetFile,
		Content: newContent,
	}, nil))

	_, mergeText := invokeTool(t, srv, ctx, "overlay_merge", map[string]any{
		"from":    "writer",
		"to_disk": true,
	})
	require.Contains(t, mergeText, `"merged":1`)
	require.Contains(t, mergeText, `"dropped_branch":true`)

	onDisk, err := os.ReadFile(targetFile)
	require.NoError(t, err)
	require.Equal(t, newContent, string(onDisk), "to_disk merge must write the overlay content verbatim")

	// Branch should be gone after to_disk.
	branches, err := srv.OverlayManager().Branches(sessID)
	require.NoError(t, err)
	for _, b := range branches {
		require.NotEqual(t, "writer", b.Name, "writer branch should be dropped after to_disk merge")
	}
}

// TestOverlayBranch_MergeToDiskDrift is requirement 10: when the
// on-disk file changes between fork and merge, the merge must
// refuse with the standard drift error (reusing gitBlobSHA).
func TestOverlayBranch_MergeToDiskDrift(t *testing.T) {
	srv, sessID, _, targetFile, _, ctx := branchBootstrap(t)

	// Compute the file's current SHA so the overlay carries an
	// anchor pointing at the pre-edit state.
	originalContent, err := os.ReadFile(targetFile)
	require.NoError(t, err)
	originalSHA := gitBlobSHAForTest(originalContent)

	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "drifter", "activate": true})
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    targetFile,
		Content: "package main\n\nfunc Target() {}\n\nfunc OverlaidEdit() {}\n",
		BaseSHA: originalSHA,
	}, nil))

	// Simulate a concurrent edit on disk between fork and merge.
	require.NoError(t, os.WriteFile(targetFile, []byte("package main\n\n// concurrent edit\nfunc Target() {}\n"), 0o644))

	res, body := invokeTool(t, srv, ctx, "overlay_merge", map[string]any{
		"from":    "drifter",
		"to_disk": true,
	})
	require.True(t, res.IsError, "drift on to_disk must surface as a tool error: %s", body)
	require.Contains(t, body, "overlay base SHA mismatch", "drift message must echo the existing overlay drift error")
}

// TestOverlayBranch_CompareBranches is requirement 11: edits that
// change the callers of a symbol surface in compare_branches as a
// differing caller set between two branches.
func TestOverlayBranch_CompareBranches(t *testing.T) {
	srv, sessID, _, _, callerFile, ctx := branchBootstrap(t)

	// Branch a: caller still calls Target.
	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "a", "activate": true})
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    callerFile,
		Content: "package main\n\nfunc Caller() {\n\tTarget()\n}\n",
	}, nil))

	// Branch b: caller stops calling Target.
	_, _ = invokeTool(t, srv, ctx, "overlay_switch", map[string]any{"name": "main"})
	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "b", "activate": true})
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    callerFile,
		Content: "package main\n\nfunc Caller() {\n}\n",
	}, nil))

	res, body := invokeTool(t, srv, ctx, "compare_branches", map[string]any{
		"a":    "a",
		"b":    "b",
		"kind": "get_callers",
		"id":   "target.go::Target",
	})
	require.False(t, res.IsError, "compare_branches: %s", body)
	require.Contains(t, body, `"a":"a"`)
	require.Contains(t, body, `"b":"b"`)
	require.Contains(t, body, `"delta"`)
	require.Contains(t, body, `"a_result"`)
	require.Contains(t, body, `"b_result"`)
	// One side should have a Caller entry, the other shouldn't.
	require.Contains(t, body, "Caller", "compare_branches must surface the differing caller set")
}

// TestOverlayBranch_BackwardCompat is requirement 12: without any
// branch tool calls, every existing overlay tool behaves identically
// to the pre-branching world (push / list / delete / drop /
// compare_with_overlay / preview_edit / simulate_chain).
func TestOverlayBranch_BackwardCompat(t *testing.T) {
	srv, sessID, _, targetFile, _, ctx := branchBootstrap(t)

	// 1. overlay_push works as before.
	_, pushText := invokeTool(t, srv, ctx, "overlay_push", map[string]any{
		"path":    targetFile,
		"content": "package main\n\nfunc Target() {}\n\nfunc Compat() {}\n",
	})
	require.Contains(t, pushText, `"path":`)

	// 2. overlay_list shows the file.
	_, listText := invokeTool(t, srv, ctx, "overlay_list", map[string]any{})
	require.Contains(t, listText, `"count":1`)
	require.Contains(t, listText, targetFile)

	// 3. A query reflects the overlay (Compat is visible).
	_, summary := invokeTool(t, srv, ctx, "get_file_summary", map[string]any{
		"path": filepath.Base(targetFile),
	})
	require.Contains(t, summary, "Compat")

	// 4. compare_with_overlay still works (uses active branch).
	_, cmp := invokeTool(t, srv, ctx, "compare_with_overlay", map[string]any{
		"kind": "find_usages",
		"id":   "target.go::Target",
	})
	require.Contains(t, cmp, `"overlay_paths"`)

	// 5. overlay_delete removes the file.
	_, delText := invokeTool(t, srv, ctx, "overlay_delete", map[string]any{"path": targetFile})
	require.Contains(t, delText, `"ok":true`)
	require.Zero(t, srv.OverlayManager().FileCount(sessID))

	// 6. overlay_drop nukes the session.
	_, _ = invokeTool(t, srv, ctx, "overlay_push", map[string]any{
		"path":    targetFile,
		"content": "package main\n\nfunc Target() {}\n\nfunc StillCompat() {}\n",
	})
	_, dropText := invokeTool(t, srv, ctx, "overlay_drop", map[string]any{})
	require.Contains(t, dropText, `"ok":true`)
	require.False(t, srv.OverlayManager().Has(sessID))
}

// TestOverlayBranch_ConcurrentBranches is requirement 13: spawn
// four goroutines each operating on a different branch
// concurrently for many iterations; the race detector must stay
// clean and the final state must match the expected per-branch
// content. The shared-mutex contract of OverlayManager is what
// makes this safe; this test is the proof.
func TestOverlayBranch_ConcurrentBranches(t *testing.T) {
	srv, sessID, dir, _, _, ctx := branchBootstrap(t)

	// Disk files so the overlay parser doesn't complain about
	// missing files (overlay graph-path resolution still works
	// for already-tracked files).
	files := []string{}
	for i := 0; i < 4; i++ {
		p := filepath.Join(dir, fmt.Sprintf("g%d.go", i))
		require.NoError(t, os.WriteFile(p, []byte("package main\n"), 0o644))
		files = append(files, p)
	}

	// Fork four named branches off main.
	branchNames := []string{"g0", "g1", "g2", "g3"}
	for _, name := range branchNames {
		_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": name})
	}

	const iterations = 100
	var wg sync.WaitGroup
	for i, name := range branchNames {
		i, name := i, name
		wg.Add(1)
		go func() {
			defer wg.Done()
			path := files[i]
			content := fmt.Sprintf("package main\n\nfunc OnBranch%d_%%d() {}\n", i)
			for j := 0; j < iterations; j++ {
				// Push into this goroutine's branch by snapshotting
				// onto it via the manager API directly. The manager
				// guarantees branch isolation under its mutex.
				err := branchPush(srv, sessID, name, daemon.OverlayFile{
					Path:    path,
					Content: fmt.Sprintf(content, j),
				})
				if err != nil {
					t.Errorf("branch %s push %d: %v", name, j, err)
					return
				}
				// Read back to exercise the read path too.
				got, err := branchRead(srv, sessID, name, path)
				if err != nil {
					t.Errorf("branch %s read %d: %v", name, j, err)
					return
				}
				wantSuffix := fmt.Sprintf("OnBranch%d_%d", i, j)
				if !strings.Contains(got, wantSuffix) {
					t.Errorf("branch %s iter %d: content %q missing %q", name, j, got, wantSuffix)
					return
				}
			}
		}()
	}
	wg.Wait()

	// Final state: every branch must hold the file with the last
	// iteration's content.
	for i, name := range branchNames {
		got, err := branchRead(srv, sessID, name, files[i])
		require.NoError(t, err)
		require.Contains(t, got, fmt.Sprintf("OnBranch%d_%d", i, iterations-1))
	}
}

// TestOverlayBranch_ActivateOption verifies the activate:true
// branch of overlay_fork: when set, the new branch becomes the
// session's active branch and subsequent queries route there.
func TestOverlayBranch_ActivateOption(t *testing.T) {
	srv, _, _, targetFile, _, ctx := branchBootstrap(t)
	_, _ = invokeTool(t, srv, ctx, "overlay_push", map[string]any{
		"path":    targetFile,
		"content": "package main\n\nfunc Target() {}\n\nfunc OnMain() {}\n",
	})
	_, forkText := invokeTool(t, srv, ctx, "overlay_fork", map[string]any{
		"name":     "active-fork",
		"activate": true,
	})
	require.Contains(t, forkText, `"active":true`)

	// After activate:true, the active branch is the new one.
	id, _ := SessionScopeForTest(srv, ctx)
	active, err := srv.OverlayManager().ActiveBranch(id)
	require.NoError(t, err)
	require.Equal(t, "active-fork", active)

	// Editing on the fork doesn't propagate back to main.
	_, _ = invokeTool(t, srv, ctx, "overlay_push", map[string]any{
		"path":    targetFile,
		"content": "package main\n\nfunc Target() {}\n\nfunc OnFork() {}\n",
	})
	_, _ = invokeTool(t, srv, ctx, "overlay_switch", map[string]any{"name": "main"})
	files, err := srv.OverlayManager().Files(id)
	require.NoError(t, err)
	require.Contains(t, files[targetFile].Content, "OnMain")
	require.NotContains(t, files[targetFile].Content, "OnFork")
}

// TestOverlayBranch_ForkFromExplicitParent verifies the `from`
// parameter: forking off a non-active branch creates a branch
// whose contents match that source, not the session's active
// branch.
func TestOverlayBranch_ForkFromExplicitParent(t *testing.T) {
	srv, sessID, _, targetFile, _, ctx := branchBootstrap(t)

	// On main, write content A.
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    targetFile,
		Content: "package main\n\nfunc Target() {}\n\nfunc OnMain() {}\n",
	}, nil))

	// Fork main→x with different content.
	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "x", "activate": true})
	require.NoError(t, srv.OverlayManager().Push(sessID, daemon.OverlayFile{
		Path:    targetFile,
		Content: "package main\n\nfunc Target() {}\n\nfunc OnX() {}\n",
	}, nil))

	// Now active is x; fork explicit from main (NOT active).
	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{
		"name": "y",
		"from": "main",
	})
	require.NoError(t, srv.OverlayManager().SwitchBranch(sessID, "y"))
	files, err := srv.OverlayManager().Files(sessID)
	require.NoError(t, err)
	require.Contains(t, files[targetFile].Content, "OnMain", "fork from=main must clone main, not the active branch")
	require.NotContains(t, files[targetFile].Content, "OnX")
}

// TestOverlayBranch_InvalidName covers the name-validation surface:
// empty / overlong / illegal-character names must surface a clear
// error before the manager is touched.
func TestOverlayBranch_InvalidName(t *testing.T) {
	srv, _, _, _, _, ctx := branchBootstrap(t)
	for _, bad := range []string{"", "../escape", "has space", strings.Repeat("a", 65), "-leading-dash"} {
		res, body := invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": bad})
		require.Truef(t, res.IsError, "invalid name %q should error; got: %s", bad, body)
	}
}

// TestOverlayBranch_ForkExisting verifies that forking onto an
// existing branch name is rejected.
func TestOverlayBranch_ForkExisting(t *testing.T) {
	srv, _, _, _, _, ctx := branchBootstrap(t)
	_, _ = invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "dup"})
	res, body := invokeTool(t, srv, ctx, "overlay_fork", map[string]any{"name": "dup"})
	require.True(t, res.IsError)
	require.Contains(t, body, "already exists")
}

// TestOverlayBranch_SwitchUnknown surfaces a clear error for
// switch-to-nonexistent.
func TestOverlayBranch_SwitchUnknown(t *testing.T) {
	srv, _, _, _, _, ctx := branchBootstrap(t)
	res, body := invokeTool(t, srv, ctx, "overlay_switch", map[string]any{"name": "ghost"})
	require.True(t, res.IsError)
	require.Contains(t, body, "not found")
}

// TestOverlayBranch_DropUnknown surfaces a clear error.
func TestOverlayBranch_DropUnknown(t *testing.T) {
	srv, _, _, _, _, ctx := branchBootstrap(t)
	res, body := invokeTool(t, srv, ctx, "overlay_drop_branch", map[string]any{"name": "ghost"})
	require.True(t, res.IsError)
	require.Contains(t, body, "not found")
}

// TestOverlayBranch_DaemonAPI_Direct exercises the daemon-side API
// directly (without going through the MCP tool wrappers) to keep
// the daemon-contract tests close to the daemon code. This
// guarantees the branching primitives stay correct even if the
// MCP surface evolves.
func TestOverlayBranch_DaemonAPI_Direct(t *testing.T) {
	m := daemon.NewOverlayManager(time.Minute)
	require.NoError(t, m.RegisterWithID("sess", "ws"))

	// Default: one branch, main, active.
	br, err := m.Branches("sess")
	require.NoError(t, err)
	require.Len(t, br, 1)
	require.Equal(t, "main", br[0].Name)
	require.True(t, br[0].Active)

	// Push into main, fork, confirm child inherits.
	require.NoError(t, m.Push("sess", daemon.OverlayFile{Path: "p.go", Content: "X"}, nil))
	fr, err := m.Fork("sess", daemon.ForkOptions{Name: "child"})
	require.NoError(t, err)
	require.Equal(t, "child", fr.Branch)
	require.Equal(t, "main", fr.Parent)
	require.Equal(t, 1, fr.FileCount)
	require.False(t, fr.Active)

	// Switch to child, modify, switch back, confirm main unchanged.
	require.NoError(t, m.SwitchBranch("sess", "child"))
	require.NoError(t, m.Push("sess", daemon.OverlayFile{Path: "p.go", Content: "Y"}, nil))
	require.NoError(t, m.SwitchBranch("sess", "main"))
	files, err := m.Files("sess")
	require.NoError(t, err)
	require.Equal(t, "X", files["p.go"].Content)

	// Cannot drop main.
	require.ErrorIs(t, m.DropBranch("sess", "main"), daemon.ErrCannotDropMainBranch)

	// Cannot drop active branch (which is currently main).
	require.NoError(t, m.SwitchBranch("sess", "child"))
	require.ErrorIs(t, m.DropBranch("sess", "child"), daemon.ErrCannotDropActiveBranch)

	// Switch off, drop succeeds.
	require.NoError(t, m.SwitchBranch("sess", "main"))
	require.NoError(t, m.DropBranch("sess", "child"))
	br, err = m.Branches("sess")
	require.NoError(t, err)
	require.Len(t, br, 1)

	// Invalid name shapes.
	for _, bad := range []string{"", "with space", "../oops"} {
		_, err := m.Fork("sess", daemon.ForkOptions{Name: bad})
		require.Error(t, err, "name %q must be rejected", bad)
	}
}

// TestOverlayBranch_MergeDaemon_Conflict checks the daemon's
// MergeBranches in isolation (no MCP transport in the loop).
func TestOverlayBranch_MergeDaemon_Conflict(t *testing.T) {
	m := daemon.NewOverlayManager(time.Minute)
	require.NoError(t, m.RegisterWithID("s", ""))

	// Two branches with conflicting content on the same path.
	require.NoError(t, m.Push("s", daemon.OverlayFile{Path: "p", Content: "A"}, nil))
	_, err := m.Fork("s", daemon.ForkOptions{Name: "b", Activate: true})
	require.NoError(t, err)
	require.NoError(t, m.Push("s", daemon.OverlayFile{Path: "p", Content: "B"}, nil))

	// Merge a (which is main) → b without force.
	res, err := m.MergeBranches("s", daemon.MergeOptions{From: "main", Into: "b"}, nil)
	require.ErrorIs(t, err, daemon.ErrMergeConflict)
	require.Equal(t, []string{"p"}, res.Conflicts)

	// b's content unchanged after aborted merge.
	files, _ := m.Files("s")
	require.Equal(t, "B", files["p"].Content)

	// With force, main wins.
	_, err = m.MergeBranches("s", daemon.MergeOptions{From: "main", Into: "b", Force: true}, nil)
	require.NoError(t, err)
	files, _ = m.Files("s")
	require.Equal(t, "A", files["p"].Content)
}

// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------

// branchPush pushes a file onto a *specific* branch without
// changing the session's active branch. Used by the concurrency
// test so four goroutines can hammer four sibling branches under
// one mutex without racing on the active pointer.
func branchPush(srv *Server, sessID, branchName string, f daemon.OverlayFile) error {
	return srv.OverlayManager().PushToBranch(sessID, branchName, f, nil)
}

// branchRead reads a single file's content from a specific branch
// without leaking the active-branch state.
func branchRead(srv *Server, sessID, branchName, path string) (string, error) {
	mgr := srv.OverlayManager()
	files, err := mgr.FilesForBranch(sessID, branchName)
	if err != nil {
		return "", err
	}
	for _, f := range files {
		if f.Path == path {
			return f.Content, nil
		}
	}
	return "", fmt.Errorf("branch %s: path %s not found", branchName, path)
}

// gitBlobSHAForTest is a tiny duplicate of the production
// gitBlobSHA helper that keeps this test file independent of the
// production helper's package-private symbol. Tests must not
// depend on whether the production helper is exported.
func gitBlobSHAForTest(data []byte) string {
	h := sha1.New()
	_, _ = fmt.Fprintf(h, "blob %d\x00", len(data))
	_, _ = h.Write(data)
	return hex.EncodeToString(h.Sum(nil))
}

// jsonEscape returns the JSON-string-safe form of s. Used in test
// assertions where the file path embedded in the response carries
// a slash (no escaping needed on POSIX) but the test wants to be
// robust against future shell-path quirks.
func jsonEscape(s string) string {
	// JSON-encoding a string adds quote characters; strip them.
	r := strings.NewReplacer(`\`, `\\`, `"`, `\"`)
	return r.Replace(s)
}

// listForActiveRowMatches checks whether the overlay_branches JSON
// payload `body` has its `active:true` entry pointing at the named
// branch. JSON-key order is not stable across encoders, so we parse
// the payload defensively rather than asserting on a substring.
func listForActiveRowMatches(body, expectedName string) bool {
	var payload struct {
		Branches []struct {
			Name   string `json:"name"`
			Active bool   `json:"active"`
		} `json:"branches"`
	}
	if err := jsonUnmarshal([]byte(body), &payload); err != nil {
		return false
	}
	for _, b := range payload.Branches {
		if b.Active {
			return b.Name == expectedName
		}
	}
	return false
}

// SessionScopeForTest exposes the session ID + scope for branch
// tests that need to peek at the manager directly. Avoids exporting
// SessionIDFromContext for production use.
func SessionScopeForTest(srv *Server, ctx context.Context) (string, string) {
	id := SessionIDFromContext(ctx)
	if srv == nil {
		return id, ""
	}
	if mgr := srv.OverlayManager(); mgr != nil {
		_, _ = mgr.ActiveBranch(id) // exercise the path
	}
	return id, ""
}

// _ daemon.ErrOverlayDrift links the import even if a refactor
// elides the only direct reference; the test file imports the
// daemon package for the OverlayFile / OverlayManager surface.
var _ = errors.Is
