package agenttool

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

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

func runRead(t *testing.T, tool *ReadTool, args map[string]any) (agentcore.AgentToolResult, bool) {
	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, false
}

func resultText(res agentcore.AgentToolResult) string {
	var b strings.Builder
	for _, c := range res.Content {
		if tc, ok := c.(agentcore.TextContent); ok {
			b.WriteString(tc.Text)
		}
	}
	return b.String()
}

func TestReadToolBasic(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "hello.txt")
	if err := os.WriteFile(path, []byte("line one\nline two\nline three\n"), 0o644); err != nil {
		t.Fatalf("write: %v", err)
	}
	tool := &ReadTool{Root: dir}
	res, _ := runRead(t, tool, map[string]any{"path": "hello.txt"})
	text := resultText(res)
	if !strings.Contains(text, "line one") || !strings.Contains(text, "line three") {
		t.Errorf("missing content: %q", text)
	}
	// Line numbers present.
	if !strings.Contains(text, "1\tline one") || !strings.Contains(text, "3\tline three") {
		t.Errorf("missing line numbers: %q", text)
	}
}

func TestReadToolOffsetLimit(t *testing.T) {
	dir := t.TempDir()
	var sb strings.Builder
	for i := 1; i <= 10; i++ {
		sb.WriteString("row\n")
	}
	path := filepath.Join(dir, "rows.txt")
	if err := os.WriteFile(path, []byte(sb.String()), 0o644); err != nil {
		t.Fatalf("write: %v", err)
	}
	tool := &ReadTool{Root: dir}
	res, _ := runRead(t, tool, map[string]any{"path": "rows.txt", "offset": 3, "limit": 2})
	text := resultText(res)
	// Should include line numbers 3 and 4, not 1,2,5.
	if !strings.Contains(text, "3\trow") || !strings.Contains(text, "4\trow") {
		t.Errorf("offset/limit window wrong: %q", text)
	}
	if strings.Contains(text, "2\trow") || strings.Contains(text, "5\trow") {
		t.Errorf("offset/limit leaked outside window: %q", text)
	}
}

func TestReadToolMissingFile(t *testing.T) {
	tool := &ReadTool{Root: t.TempDir()}
	res, _ := runRead(t, tool, map[string]any{"path": "nope.txt"})
	if !strings.Contains(resultText(res), "does not exist") {
		t.Errorf("expected does-not-exist error, got %q", resultText(res))
	}
}

func TestReadToolPathTraversal(t *testing.T) {
	dir := t.TempDir()
	// A secret sits outside the root.
	parent := filepath.Dir(dir)
	secret := filepath.Join(parent, "secret.txt")
	if err := os.WriteFile(secret, []byte("top secret"), 0o644); err != nil {
		t.Fatalf("write secret: %v", err)
	}
	defer os.Remove(secret)

	tool := &ReadTool{Root: dir}
	res, _ := runRead(t, tool, map[string]any{"path": "../secret.txt"})
	text := resultText(res)
	if strings.Contains(text, "top secret") {
		t.Fatal("path traversal escaped the root!")
	}
	if !strings.Contains(text, "outside the workspace root") {
		t.Errorf("expected boundary error, got %q", text)
	}
}

func TestReadToolDirectory(t *testing.T) {
	dir := t.TempDir()
	sub := filepath.Join(dir, "subdir")
	if err := os.Mkdir(sub, 0o755); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	tool := &ReadTool{Root: dir}
	res, _ := runRead(t, tool, map[string]any{"path": "subdir"})
	if !strings.Contains(resultText(res), "is a directory") {
		t.Errorf("expected directory error, got %q", resultText(res))
	}
}

func TestReadToolTruncation(t *testing.T) {
	dir := t.TempDir()
	var sb strings.Builder
	for i := 0; i < readToolMaxLines+50; i++ {
		sb.WriteString("x\n")
	}
	path := filepath.Join(dir, "big.txt")
	if err := os.WriteFile(path, []byte(sb.String()), 0o644); err != nil {
		t.Fatalf("write: %v", err)
	}
	tool := &ReadTool{Root: dir}
	res, _ := runRead(t, tool, map[string]any{"path": "big.txt"})
	if !strings.Contains(resultText(res), "output truncated") {
		t.Error("expected truncation notice for oversized file")
	}
}

func TestReadToolMissingPathArg(t *testing.T) {
	tool := &ReadTool{Root: t.TempDir()}
	res, _ := runRead(t, tool, map[string]any{})
	if !strings.Contains(resultText(res), "path is required") {
		t.Errorf("expected path-required error, got %q", resultText(res))
	}
}

func TestReadToolExtraRootsAllowsTrustedOutsidePath(t *testing.T) {
	work := t.TempDir()
	// A skill file lives OUTSIDE the workspace root (mirrors ~/.agents/skills).
	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("skill body"), 0o644); err != nil {
		t.Fatalf("write skill: %v", err)
	}

	// Without ExtraRoots the absolute skill path is rejected as out-of-workspace.
	bounded := &ReadTool{Root: work}
	res, _ := runRead(t, bounded, map[string]any{"path": skillFile})
	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 same read succeeds.
	tool := &ReadTool{Root: work, ExtraRoots: []string{skills}}
	res, _ = runRead(t, tool, map[string]any{"path": skillFile})
	if !strings.Contains(resultText(res), "skill body") {
		t.Fatalf("expected skill contents with ExtraRoots, got %q", resultText(res))
	}
}

func TestReadToolExtraRootsStillBlocksUntrustedPath(t *testing.T) {
	work := t.TempDir()
	skills := t.TempDir()
	// A secret sits outside BOTH the workspace root and the extra root.
	other := t.TempDir()
	secret := filepath.Join(other, "secret.txt")
	if err := os.WriteFile(secret, []byte("top secret"), 0o644); err != nil {
		t.Fatalf("write secret: %v", err)
	}

	tool := &ReadTool{Root: work, ExtraRoots: []string{skills}}
	res, _ := runRead(t, tool, map[string]any{"path": secret})
	text := resultText(res)
	if strings.Contains(text, "top secret") {
		t.Fatal("read escaped both roots!")
	}
	if !strings.Contains(text, "outside the workspace root") {
		t.Errorf("expected boundary error for untrusted path, got %q", text)
	}
}

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