package fstools

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

// TestEditTool_EditFile_Success verifies successful file editing
func TestEditTool_EditFile_Success(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.txt")
	os.WriteFile(testFile, []byte("Hello World\nThis is a test"), 0o644)

	tool := NewEditFileTool(tmpDir, true)
	ctx := context.Background()
	args := map[string]any{
		"path":     testFile,
		"old_text": "World",
		"new_text": "Universe",
	}

	result := tool.Execute(ctx, args)

	// Success should not be an error
	if result.IsError {
		t.Errorf("Expected success, got IsError=true: %s", result.ForLLM)
	}

	// Successful edits should surface a diff to the user.
	if result.Silent {
		t.Errorf("Expected Silent=false for EditFile, got true")
	}

	if result.ForUser == "" {
		t.Fatal("Expected ForUser to contain the diff preview")
	}

	if result.ForLLM == result.ForUser {
		t.Fatalf("Expected ForLLM to be a compact summary, got identical outputs %q", result.ForLLM)
	}
	if result.ForLLM != fmt.Sprintf("File edited: %s", testFile) {
		t.Fatalf("Expected compact ForLLM summary, got %q", result.ForLLM)
	}

	diffPath := strings.TrimLeft(filepath.ToSlash(testFile), "/")
	for _, want := range []string{
		fmt.Sprintf("File edited: %s", testFile),
		"```diff",
		"--- a/" + diffPath,
		"+++ b/" + diffPath,
		"-Hello World",
		"+Hello Universe",
	} {
		if !strings.Contains(result.ForUser, want) {
			t.Fatalf("Expected edit diff to contain %q, got:\n%s", want, result.ForUser)
		}
	}

	// Verify file was actually edited
	content, err := os.ReadFile(testFile)
	if err != nil {
		t.Fatalf("Failed to read edited file: %v", err)
	}
	contentStr := string(content)
	if !strings.Contains(contentStr, "Hello Universe") {
		t.Errorf("Expected file to contain 'Hello Universe', got: %s", contentStr)
	}
	if strings.Contains(contentStr, "Hello World") {
		t.Errorf("Expected 'Hello World' to be replaced, got: %s", contentStr)
	}
}

// TestEditTool_EditFile_NotFound verifies error handling for non-existent file
func TestEditTool_EditFile_NotFound(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "nonexistent.txt")

	tool := NewEditFileTool(tmpDir, true)
	ctx := context.Background()
	args := map[string]any{
		"path":     testFile,
		"old_text": "old",
		"new_text": "new",
	}

	result := tool.Execute(ctx, args)

	// Should return error result
	if !result.IsError {
		t.Errorf("Expected error for non-existent file")
	}

	// Should mention file not found
	if !strings.Contains(result.ForLLM, "not found") && !strings.Contains(result.ForUser, "not found") {
		t.Errorf("Expected 'file not found' message, got ForLLM: %s", result.ForLLM)
	}
}

// TestEditTool_EditFile_OldTextNotFound verifies error when old_text doesn't exist
func TestEditTool_EditFile_OldTextNotFound(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.txt")
	os.WriteFile(testFile, []byte("Hello World"), 0o644)

	tool := NewEditFileTool(tmpDir, true)
	ctx := context.Background()
	args := map[string]any{
		"path":     testFile,
		"old_text": "Goodbye",
		"new_text": "Hello",
	}

	result := tool.Execute(ctx, args)

	// Should return error result
	if !result.IsError {
		t.Errorf("Expected error when old_text not found")
	}

	// Should mention old_text not found
	if !strings.Contains(result.ForLLM, "not found") && !strings.Contains(result.ForUser, "not found") {
		t.Errorf("Expected 'not found' message, got ForLLM: %s", result.ForLLM)
	}
}

// TestEditTool_EditFile_MultipleMatches verifies error when old_text appears multiple times
func TestEditTool_EditFile_MultipleMatches(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.txt")
	os.WriteFile(testFile, []byte("test test test"), 0o644)

	tool := NewEditFileTool(tmpDir, true)
	ctx := context.Background()
	args := map[string]any{
		"path":     testFile,
		"old_text": "test",
		"new_text": "done",
	}

	result := tool.Execute(ctx, args)

	// Should return error result
	if !result.IsError {
		t.Errorf("Expected error when old_text appears multiple times")
	}

	// Should mention multiple occurrences
	if !strings.Contains(result.ForLLM, "times") && !strings.Contains(result.ForUser, "times") {
		t.Errorf("Expected 'multiple times' message, got ForLLM: %s", result.ForLLM)
	}
}

// TestEditTool_EditFile_OutsideAllowedDir verifies error when path is outside allowed directory
func TestEditTool_EditFile_OutsideAllowedDir(t *testing.T) {
	tmpDir := t.TempDir()
	otherDir := t.TempDir()
	testFile := filepath.Join(otherDir, "test.txt")
	os.WriteFile(testFile, []byte("content"), 0o644)

	tool := NewEditFileTool(tmpDir, true) // Restrict to tmpDir
	ctx := context.Background()
	args := map[string]any{
		"path":     testFile,
		"old_text": "content",
		"new_text": "new",
	}

	result := tool.Execute(ctx, args)

	// Should return error result
	assert.True(t, result.IsError, "Expected error when path is outside allowed directory")

	// Should mention outside allowed directory
	// Note: ErrorResult only sets ForLLM by default, so ForUser might be empty.
	// We check ForLLM as it's the primary error channel.
	assert.True(
		t,
		strings.Contains(result.ForLLM, "outside") || strings.Contains(result.ForLLM, "access denied") ||
			strings.Contains(result.ForLLM, "escapes"),
		"Expected 'outside allowed' or 'access denied' message, got ForLLM: %s",
		result.ForLLM,
	)
}

// TestEditTool_EditFile_MissingPath verifies error handling for missing path
func TestEditTool_EditFile_MissingPath(t *testing.T) {
	tool := NewEditFileTool("", false)
	ctx := context.Background()
	args := map[string]any{
		"old_text": "old",
		"new_text": "new",
	}

	result := tool.Execute(ctx, args)

	// Should return error result
	if !result.IsError {
		t.Errorf("Expected error when path is missing")
	}
}

// TestEditTool_EditFile_MissingOldText verifies error handling for missing old_text
func TestEditTool_EditFile_MissingOldText(t *testing.T) {
	tool := NewEditFileTool("", false)
	ctx := context.Background()
	args := map[string]any{
		"path":     "/tmp/test.txt",
		"new_text": "new",
	}

	result := tool.Execute(ctx, args)

	// Should return error result
	if !result.IsError {
		t.Errorf("Expected error when old_text is missing")
	}
}

// TestEditTool_EditFile_MissingNewText verifies error handling for missing new_text
func TestEditTool_EditFile_MissingNewText(t *testing.T) {
	tool := NewEditFileTool("", false)
	ctx := context.Background()
	args := map[string]any{
		"path":     "/tmp/test.txt",
		"old_text": "old",
	}

	result := tool.Execute(ctx, args)

	// Should return error result
	if !result.IsError {
		t.Errorf("Expected error when new_text is missing")
	}
}

// TestEditTool_AppendFile_Success verifies successful file appending
func TestEditTool_AppendFile_Success(t *testing.T) {
	tmpDir := t.TempDir()
	testFile := filepath.Join(tmpDir, "test.txt")
	os.WriteFile(testFile, []byte("Initial content"), 0o644)

	tool := NewAppendFileTool("", false)
	ctx := context.Background()
	args := map[string]any{
		"path":    testFile,
		"content": "\nAppended content",
	}

	result := tool.Execute(ctx, args)

	// Success should not be an error
	if result.IsError {
		t.Errorf("Expected success, got IsError=true: %s", result.ForLLM)
	}

	// Should return SilentResult
	if !result.Silent {
		t.Errorf("Expected Silent=true for AppendFile, got false")
	}

	// ForUser should be empty (silent result)
	if result.ForUser != "" {
		t.Errorf("Expected ForUser to be empty for SilentResult, got: %s", result.ForUser)
	}

	// Verify content was actually appended
	content, err := os.ReadFile(testFile)
	if err != nil {
		t.Fatalf("Failed to read file: %v", err)
	}
	contentStr := string(content)
	if !strings.Contains(contentStr, "Initial content") {
		t.Errorf("Expected original content to remain, got: %s", contentStr)
	}
	if !strings.Contains(contentStr, "Appended content") {
		t.Errorf("Expected appended content, got: %s", contentStr)
	}
}

// TestEditTool_AppendFile_MissingPath verifies error handling for missing path
func TestEditTool_AppendFile_MissingPath(t *testing.T) {
	tool := NewAppendFileTool("", false)
	ctx := context.Background()
	args := map[string]any{
		"content": "test",
	}

	result := tool.Execute(ctx, args)

	// Should return error result
	if !result.IsError {
		t.Errorf("Expected error when path is missing")
	}
}

// TestEditTool_AppendFile_MissingContent verifies error handling for missing content
func TestEditTool_AppendFile_MissingContent(t *testing.T) {
	tool := NewAppendFileTool("", false)
	ctx := context.Background()
	args := map[string]any{
		"path": "/tmp/test.txt",
	}

	result := tool.Execute(ctx, args)

	// Should return error result
	if !result.IsError {
		t.Errorf("Expected error when content is missing")
	}
}

// TestReplaceEditContent verifies the helper function replaceEditContent
func TestReplaceEditContent(t *testing.T) {
	tests := []struct {
		name        string
		content     []byte
		oldText     string
		newText     string
		expected    []byte
		expectError bool
	}{
		{
			name:        "successful replacement",
			content:     []byte("hello world"),
			oldText:     "world",
			newText:     "universe",
			expected:    []byte("hello universe"),
			expectError: false,
		},
		{
			name:        "old text not found",
			content:     []byte("hello world"),
			oldText:     "golang",
			newText:     "rust",
			expected:    nil,
			expectError: true,
		},
		{
			name:        "multiple matches found",
			content:     []byte("test text test"),
			oldText:     "test",
			newText:     "done",
			expected:    nil,
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, err := replaceEditContent(tt.content, tt.oldText, tt.newText)
			if tt.expectError {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tt.expected, result)
			}
		})
	}
}

// TestAppendFileTool_AppendToNonExistent_Restricted verifies that AppendFileTool in restricted mode
// can append to a file that does not yet exist — it should silently create the file.
// This exercises the errors.Is(err, fs.ErrNotExist) path in appendFileWithRW + rootRW.
func TestAppendFileTool_AppendToNonExistent_Restricted(t *testing.T) {
	workspace := t.TempDir()
	tool := NewAppendFileTool(workspace, true)
	ctx := context.Background()

	args := map[string]any{
		"path":    "brand_new_file.txt",
		"content": "first content",
	}

	result := tool.Execute(ctx, args)
	assert.False(
		t,
		result.IsError,
		"Expected success when appending to non-existent file in restricted mode, got: %s",
		result.ForLLM,
	)

	// Verify the file was created with correct content
	data, err := os.ReadFile(filepath.Join(workspace, "brand_new_file.txt"))
	assert.NoError(t, err)
	assert.Equal(t, "first content", string(data))
}

// TestAppendFileTool_Restricted_Success verifies that AppendFileTool in restricted mode
// correctly appends to an existing file within the sandbox.
func TestAppendFileTool_Restricted_Success(t *testing.T) {
	workspace := t.TempDir()
	testFile := "existing.txt"
	err := os.WriteFile(filepath.Join(workspace, testFile), []byte("initial"), 0o644)
	assert.NoError(t, err)

	tool := NewAppendFileTool(workspace, true)
	ctx := context.Background()
	args := map[string]any{
		"path":    testFile,
		"content": " appended",
	}

	result := tool.Execute(ctx, args)
	assert.False(t, result.IsError, "Expected success, got: %s", result.ForLLM)
	assert.True(t, result.Silent)

	data, err := os.ReadFile(filepath.Join(workspace, testFile))
	assert.NoError(t, err)
	assert.Equal(t, "initial appended", string(data))
}

// TestEditFileTool_Restricted_InPlaceEdit verifies that EditFileTool in restricted mode
// correctly edits a file using the single-open editFileInRoot path.
func TestEditFileTool_Restricted_InPlaceEdit(t *testing.T) {
	workspace := t.TempDir()
	testFile := "edit_target.txt"
	err := os.WriteFile(filepath.Join(workspace, testFile), []byte("Hello World"), 0o644)
	assert.NoError(t, err)

	tool := NewEditFileTool(workspace, true)
	ctx := context.Background()
	args := map[string]any{
		"path":     testFile,
		"old_text": "World",
		"new_text": "Go",
	}

	result := tool.Execute(ctx, args)
	assert.False(t, result.IsError, "Expected success, got: %s", result.ForLLM)
	assert.False(t, result.Silent)
	assert.Equal(t, "File edited: edit_target.txt", result.ForLLM)
	assert.Contains(t, result.ForUser, "```diff")
	assert.Contains(t, result.ForUser, "--- a/edit_target.txt")
	assert.Contains(t, result.ForUser, "+++ b/edit_target.txt")
	assert.Contains(t, result.ForUser, "-Hello World")
	assert.Contains(t, result.ForUser, "+Hello Go")

	data, err := os.ReadFile(filepath.Join(workspace, testFile))
	assert.NoError(t, err)
	assert.Equal(t, "Hello Go", string(data))
}

// TestEditFileTool_Restricted_FileNotFound verifies that editFileInRoot returns a proper
// error message when the target file does not exist.
func TestEditFileTool_Restricted_FileNotFound(t *testing.T) {
	workspace := t.TempDir()
	tool := NewEditFileTool(workspace, true)
	ctx := context.Background()
	args := map[string]any{
		"path":     "no_such_file.txt",
		"old_text": "old",
		"new_text": "new",
	}

	result := tool.Execute(ctx, args)
	assert.True(t, result.IsError)
	assert.Contains(t, result.ForLLM, "not found")
}
