1package tools
  2
  3import (
  4	"context"
  5	"os"
  6	"path/filepath"
  7	"testing"
  8
  9	"github.com/charmbracelet/crush/internal/csync"
 10	"github.com/charmbracelet/crush/internal/history"
 11	"github.com/charmbracelet/crush/internal/lsp"
 12	"github.com/charmbracelet/crush/internal/permission"
 13	"github.com/charmbracelet/crush/internal/pubsub"
 14	"github.com/stretchr/testify/require"
 15)
 16
 17type mockPermissionService struct {
 18	*pubsub.Broker[permission.PermissionRequest]
 19}
 20
 21func (m *mockPermissionService) Request(req permission.CreatePermissionRequest) bool {
 22	return true
 23}
 24
 25func (m *mockPermissionService) Grant(req permission.PermissionRequest) {}
 26
 27func (m *mockPermissionService) Deny(req permission.PermissionRequest) {}
 28
 29func (m *mockPermissionService) GrantPersistent(req permission.PermissionRequest) {}
 30
 31func (m *mockPermissionService) AutoApproveSession(sessionID string) {}
 32
 33func (m *mockPermissionService) SetSkipRequests(skip bool) {}
 34
 35func (m *mockPermissionService) SkipRequests() bool {
 36	return false
 37}
 38
 39func (m *mockPermissionService) SubscribeNotifications(ctx context.Context) <-chan pubsub.Event[permission.PermissionNotification] {
 40	return make(<-chan pubsub.Event[permission.PermissionNotification])
 41}
 42
 43type mockHistoryService struct {
 44	*pubsub.Broker[history.File]
 45}
 46
 47func (m *mockHistoryService) Create(ctx context.Context, sessionID, path, content string) (history.File, error) {
 48	return history.File{Path: path, Content: content}, nil
 49}
 50
 51func (m *mockHistoryService) CreateVersion(ctx context.Context, sessionID, path, content string) (history.File, error) {
 52	return history.File{}, nil
 53}
 54
 55func (m *mockHistoryService) GetByPathAndSession(ctx context.Context, path, sessionID string) (history.File, error) {
 56	return history.File{Path: path, Content: ""}, nil
 57}
 58
 59func (m *mockHistoryService) Get(ctx context.Context, id string) (history.File, error) {
 60	return history.File{}, nil
 61}
 62
 63func (m *mockHistoryService) ListBySession(ctx context.Context, sessionID string) ([]history.File, error) {
 64	return nil, nil
 65}
 66
 67func (m *mockHistoryService) ListLatestSessionFiles(ctx context.Context, sessionID string) ([]history.File, error) {
 68	return nil, nil
 69}
 70
 71func (m *mockHistoryService) Delete(ctx context.Context, id string) error {
 72	return nil
 73}
 74
 75func (m *mockHistoryService) DeleteSessionFiles(ctx context.Context, sessionID string) error {
 76	return nil
 77}
 78
 79func TestApplyEditToContentPartialSuccess(t *testing.T) {
 80	t.Parallel()
 81
 82	content := "line 1\nline 2\nline 3\n"
 83
 84	// Test successful edit.
 85	newContent, err := applyEditToContent(content, MultiEditOperation{
 86		OldString: "line 1",
 87		NewString: "LINE 1",
 88	})
 89	require.NoError(t, err)
 90	require.Contains(t, newContent, "LINE 1")
 91	require.Contains(t, newContent, "line 2")
 92
 93	// Test failed edit (string not found).
 94	_, err = applyEditToContent(content, MultiEditOperation{
 95		OldString: "line 99",
 96		NewString: "LINE 99",
 97	})
 98	require.Error(t, err)
 99	require.Contains(t, err.Error(), "not found")
100}
101
102func TestMultiEditSequentialApplication(t *testing.T) {
103	t.Parallel()
104
105	tmpDir := t.TempDir()
106	testFile := filepath.Join(tmpDir, "test.txt")
107
108	// Create test file.
109	content := "line 1\nline 2\nline 3\nline 4\n"
110	err := os.WriteFile(testFile, []byte(content), 0o644)
111	require.NoError(t, err)
112
113	// Mock components.
114	lspClients := csync.NewMap[string, *lsp.Client]()
115	permissions := &mockPermissionService{Broker: pubsub.NewBroker[permission.PermissionRequest]()}
116	files := &mockHistoryService{Broker: pubsub.NewBroker[history.File]()}
117
118	// Create multiedit tool.
119	_ = NewMultiEditTool(lspClients, permissions, files, tmpDir)
120
121	// Simulate reading the file first.
122	recordFileRead(testFile)
123
124	// Manually test the sequential application logic.
125	currentContent := content
126
127	// Apply edits sequentially, tracking failures.
128	edits := []MultiEditOperation{
129		{OldString: "line 1", NewString: "LINE 1"},   // Should succeed
130		{OldString: "line 99", NewString: "LINE 99"}, // Should fail - doesn't exist
131		{OldString: "line 3", NewString: "LINE 3"},   // Should succeed
132		{OldString: "line 2", NewString: "LINE 2"},   // Should succeed - still exists
133	}
134
135	var failedEdits []FailedEdit
136	successCount := 0
137
138	for i, edit := range edits {
139		newContent, err := applyEditToContent(currentContent, edit)
140		if err != nil {
141			failedEdits = append(failedEdits, FailedEdit{
142				Index: i + 1,
143				Error: err.Error(),
144				Edit:  edit,
145			})
146			continue
147		}
148		currentContent = newContent
149		successCount++
150	}
151
152	// Verify results.
153	require.Equal(t, 3, successCount, "Expected 3 successful edits")
154	require.Len(t, failedEdits, 1, "Expected 1 failed edit")
155
156	// Check failed edit details.
157	require.Equal(t, 2, failedEdits[0].Index)
158	require.Contains(t, failedEdits[0].Error, "not found")
159
160	// Verify content changes.
161	require.Contains(t, currentContent, "LINE 1")
162	require.Contains(t, currentContent, "LINE 2")
163	require.Contains(t, currentContent, "LINE 3")
164	require.Contains(t, currentContent, "line 4") // Original unchanged
165	require.NotContains(t, currentContent, "LINE 99")
166}
167
168func TestMultiEditAllEditsSucceed(t *testing.T) {
169	t.Parallel()
170
171	content := "line 1\nline 2\nline 3\n"
172
173	edits := []MultiEditOperation{
174		{OldString: "line 1", NewString: "LINE 1"},
175		{OldString: "line 2", NewString: "LINE 2"},
176		{OldString: "line 3", NewString: "LINE 3"},
177	}
178
179	currentContent := content
180	successCount := 0
181
182	for _, edit := range edits {
183		newContent, err := applyEditToContent(currentContent, edit)
184		if err != nil {
185			t.Fatalf("Unexpected error: %v", err)
186		}
187		currentContent = newContent
188		successCount++
189	}
190
191	require.Equal(t, 3, successCount)
192	require.Contains(t, currentContent, "LINE 1")
193	require.Contains(t, currentContent, "LINE 2")
194	require.Contains(t, currentContent, "LINE 3")
195}
196
197func TestMultiEditAllEditsFail(t *testing.T) {
198	t.Parallel()
199
200	content := "line 1\nline 2\n"
201
202	edits := []MultiEditOperation{
203		{OldString: "line 99", NewString: "LINE 99"},
204		{OldString: "line 100", NewString: "LINE 100"},
205	}
206
207	currentContent := content
208	var failedEdits []FailedEdit
209
210	for i, edit := range edits {
211		newContent, err := applyEditToContent(currentContent, edit)
212		if err != nil {
213			failedEdits = append(failedEdits, FailedEdit{
214				Index: i + 1,
215				Error: err.Error(),
216				Edit:  edit,
217			})
218			continue
219		}
220		currentContent = newContent
221	}
222
223	require.Len(t, failedEdits, 2)
224	require.Equal(t, content, currentContent, "Content should be unchanged")
225}