package agenttool

import (
	"context"
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/smallnest/pigo/internal/agentcore"
)

func runEdit(t *testing.T, tool *EditTool, args map[string]any) agentcore.AgentToolResult {
	t.Helper()
	raw, err := json.Marshal(args)
	if err != nil {
		t.Fatalf("marshal args: %v", err)
	}
	res, gerr := tool.Execute(context.Background(), "call-1", raw, nil)
	if gerr != nil {
		t.Fatalf("execute returned go error: %v", gerr)
	}
	return res
}

func seedFile(t *testing.T, dir, name, content string) string {
	t.Helper()
	p := filepath.Join(dir, name)
	if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
		t.Fatalf("seed %s: %v", name, err)
	}
	return p
}

func TestEditToolUniqueMatch(t *testing.T) {
	dir := t.TempDir()
	p := seedFile(t, dir, "f.txt", "alpha\nbeta\ngamma\n")
	tool := &EditTool{Root: dir}
	res := runEdit(t, tool, map[string]any{"path": "f.txt", "old_string": "beta", "new_string": "BETA"})
	if strings.Contains(resultText(res), "not found") || strings.Contains(resultText(res), "not unique") {
		t.Fatalf("unexpected error: %q", resultText(res))
	}
	got, _ := os.ReadFile(p)
	if string(got) != "alpha\nBETA\ngamma\n" {
		t.Errorf("content = %q", got)
	}
	// Diff present.
	if !strings.Contains(resultText(res), "-beta") || !strings.Contains(resultText(res), "+BETA") {
		t.Errorf("diff missing markers: %q", resultText(res))
	}
}

func TestEditToolNonUniqueErrors(t *testing.T) {
	dir := t.TempDir()
	seedFile(t, dir, "f.txt", "x\nx\nx\n")
	tool := &EditTool{Root: dir}
	res := runEdit(t, tool, map[string]any{"path": "f.txt", "old_string": "x", "new_string": "y"})
	if !strings.Contains(resultText(res), "not unique") {
		t.Errorf("expected non-unique error, got %q", resultText(res))
	}
	// File unchanged.
	got, _ := os.ReadFile(filepath.Join(dir, "f.txt"))
	if string(got) != "x\nx\nx\n" {
		t.Errorf("file should be unchanged, got %q", got)
	}
}

func TestEditToolReplaceAll(t *testing.T) {
	dir := t.TempDir()
	p := seedFile(t, dir, "f.txt", "x\nx\nx\n")
	tool := &EditTool{Root: dir}
	res := runEdit(t, tool, map[string]any{"path": "f.txt", "old_string": "x", "new_string": "y", "replace_all": true})
	got, _ := os.ReadFile(p)
	if string(got) != "y\ny\ny\n" {
		t.Errorf("content = %q, want all replaced", got)
	}
	details, ok := res.Details.(map[string]any)
	if !ok || details["replacements"] != 3 {
		t.Errorf("expected 3 replacements, details = %+v", res.Details)
	}
}

func TestEditToolNotFound(t *testing.T) {
	dir := t.TempDir()
	seedFile(t, dir, "f.txt", "hello\n")
	tool := &EditTool{Root: dir}
	res := runEdit(t, tool, map[string]any{"path": "f.txt", "old_string": "missing", "new_string": "x"})
	if !strings.Contains(resultText(res), "not found") {
		t.Errorf("expected not-found error, got %q", resultText(res))
	}
}

func TestEditToolMissingFile(t *testing.T) {
	tool := &EditTool{Root: t.TempDir()}
	res := runEdit(t, tool, map[string]any{"path": "nope.txt", "old_string": "a", "new_string": "b"})
	if !strings.Contains(resultText(res), "does not exist") {
		t.Errorf("expected does-not-exist, got %q", resultText(res))
	}
}

func TestEditToolIdenticalStrings(t *testing.T) {
	dir := t.TempDir()
	seedFile(t, dir, "f.txt", "a\n")
	tool := &EditTool{Root: dir}
	res := runEdit(t, tool, map[string]any{"path": "f.txt", "old_string": "a", "new_string": "a"})
	if !strings.Contains(resultText(res), "identical") {
		t.Errorf("expected identical error, got %q", resultText(res))
	}
}

func TestEditToolPathTraversal(t *testing.T) {
	dir := t.TempDir()
	tool := &EditTool{Root: dir}
	res := runEdit(t, tool, map[string]any{"path": "../x.txt", "old_string": "a", "new_string": "b"})
	if !strings.Contains(resultText(res), "outside the workspace root") {
		t.Errorf("expected boundary error, got %q", resultText(res))
	}
}

func TestEditToolExtraRootsAllowsSkillModification(t *testing.T) {
	work := t.TempDir()
	skills := t.TempDir()
	skillFile := filepath.Join(skills, "weather", "SKILL.md")
	if err := os.MkdirAll(filepath.Dir(skillFile), 0o755); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	if err := os.WriteFile(skillFile, []byte("old body\n"), 0o644); err != nil {
		t.Fatalf("seed skill: %v", err)
	}

	// Without ExtraRoots the out-of-workspace skill edit is rejected.
	bounded := &EditTool{Root: work}
	res := runEdit(t, bounded, map[string]any{"path": skillFile, "old_string": "old body", "new_string": "new body"})
	if !strings.Contains(resultText(res), "outside the workspace root") {
		t.Fatalf("expected boundary rejection without ExtraRoots, got %q", resultText(res))
	}

	// With the skills dir as an extra root the edit applies.
	tool := &EditTool{Root: work, ExtraRoots: []string{skills}}
	res = runEdit(t, tool, map[string]any{"path": skillFile, "old_string": "old body", "new_string": "new body"})
	if strings.Contains(resultText(res), "outside the workspace root") {
		t.Fatalf("edit still blocked with ExtraRoots: %q", resultText(res))
	}
	got, _ := os.ReadFile(skillFile)
	if !strings.Contains(string(got), "new body") {
		t.Fatalf("skill not modified, content = %q", got)
	}
}

func TestEditToolMode(t *testing.T) {
	tool := &EditTool{}
	if tool.Name() != "edit" {
		t.Errorf("name = %q", tool.Name())
	}
	if tool.ExecutionMode() != agentcore.ToolExecutionSequential {
		t.Error("edit should be sequential")
	}
	var schema map[string]any
	if err := json.Unmarshal(tool.Schema(), &schema); err != nil {
		t.Errorf("schema not valid JSON: %v", err)
	}
}

func TestUnifiedDiff(t *testing.T) {
	diff := unifiedDiff("f.txt", "a\nb\nc\n", "a\nB\nc\n")
	if !strings.Contains(diff, "--- a/f.txt") || !strings.Contains(diff, "+++ b/f.txt") {
		t.Errorf("missing header: %q", diff)
	}
	if !strings.Contains(diff, "-b") || !strings.Contains(diff, "+B") {
		t.Errorf("missing change lines: %q", diff)
	}
	// Unchanged context lines carry a leading space.
	if !strings.Contains(diff, " a") || !strings.Contains(diff, " c") {
		t.Errorf("missing context lines: %q", diff)
	}
}
