package history

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

	"reasonix/internal/agent"
	"reasonix/internal/provider"
)

func TestHistoryToolSearchAndAroundAreUsable(t *testing.T) {
	sessionDir := t.TempDir()
	path := filepath.Join(sessionDir, "decision.jsonl")
	writeSession(t, path, []provider.Message{
		{Role: provider.RoleUser, Content: "Should history use vector embeddings?"},
		{Role: provider.RoleAssistant, Content: "Decision: keep history retrieval lightweight with BM25 and no vector database."},
		{Role: provider.RoleUser, Content: "Great, port that to Reasonix."},
	})

	tl := NewTool(Options{SessionDir: sessionDir})
	if tl.Name() != "history" || !tl.ReadOnly() {
		t.Fatalf("unexpected tool identity: name=%q readonly=%v", tl.Name(), tl.ReadOnly())
	}
	if !json.Valid(tl.Schema()) {
		t.Fatal("history schema is not valid JSON")
	}

	out, err := tl.Execute(context.Background(), []byte(`{"operation":"search","query":"BM25 vector database","limit":5}`))
	if err != nil {
		t.Fatalf("Execute search: %v", err)
	}
	for _, want := range []string{
		"History search results",
		"decision.jsonl",
		"message_index=1",
		"keep history retrieval lightweight",
		`Use operation="around"`,
	} {
		if !strings.Contains(out, want) {
			t.Fatalf("search output missing %q:\n%s", want, out)
		}
	}

	args, _ := json.Marshal(map[string]any{
		"operation":     "around",
		"session_path":  path,
		"message_index": 1,
		"before":        1,
		"after":         1,
	})
	out, err = tl.Execute(context.Background(), args)
	if err != nil {
		t.Fatalf("Execute around: %v", err)
	}
	for _, want := range []string{
		"History around",
		"[0 user]",
		"[1 assistant]",
		"[2 user]",
	} {
		if !strings.Contains(out, want) {
			t.Fatalf("around output missing %q:\n%s", want, out)
		}
	}
}

func TestHistoryToolSkipsCleanupPending(t *testing.T) {
	sessionDir := t.TempDir()
	visiblePath := filepath.Join(sessionDir, "visible.jsonl")
	pendingPath := filepath.Join(sessionDir, "pending.jsonl")
	writeSession(t, visiblePath, []provider.Message{
		{Role: provider.RoleUser, Content: "visible cleanup-safe retrieval note"},
	})
	writeSession(t, pendingPath, []provider.Message{
		{Role: provider.RoleUser, Content: "pending cleanup-hidden retrieval note"},
	})
	if err := agent.MarkCleanupPending(pendingPath, "delete"); err != nil {
		t.Fatal(err)
	}

	tl := NewTool(Options{SessionDir: sessionDir})
	out, err := tl.Execute(context.Background(), []byte(`{"operation":"search","query":"retrieval note","limit":5}`))
	if err != nil {
		t.Fatalf("Execute search: %v", err)
	}
	if !strings.Contains(out, "visible.jsonl") {
		t.Fatalf("search output missing visible session:\n%s", out)
	}
	if strings.Contains(out, "pending.jsonl") || strings.Contains(out, "cleanup-hidden") {
		t.Fatalf("search output leaked cleanup-pending session:\n%s", out)
	}

	args, _ := json.Marshal(map[string]any{
		"operation":     "around",
		"session_path":  pendingPath,
		"message_index": 0,
	})
	if _, err := tl.Execute(context.Background(), args); err == nil {
		t.Fatal("Execute around cleanup-pending error = nil, want rejection")
	}
}

func TestHistoryToolSchemaIsCacheStable(t *testing.T) {
	tl := NewTool(Options{SessionDir: t.TempDir()})
	if got, want := tl.Description(), "Search saved local session history with lightweight BM25 retrieval, then read messages around a hit. Use search when past decisions, failed attempts, commands, or tool inputs may help the current task; use around with a returned session_path and message_index to inspect the nearby transcript. By default it searches user text, assistant text, tool inputs, and tool errors; normal tool outputs are excluded unless kind includes tool_output."; got != want {
		t.Fatalf("history description changed; this is provider-visible and affects prompt-cache shape.\nwant: %q\n got: %q", want, got)
	}
	const wantSchema = `{
		"type": "object",
		"properties": {
			"operation": {"type": "string", "enum": ["search", "around"], "description": "search ranks saved history; around returns nearby messages for a search hit."},
			"query": {"type": "string", "description": "Search query for operation=search."},
			"scope": {"type": "string", "enum": ["project", "global"], "description": "project searches the current session directory; global also includes compacted-history archives."},
			"kind": {"type": "array", "items": {"type": "string", "enum": ["user_text", "assistant_text", "tool_input", "tool_error", "tool_output"]}, "description": "History parts to search. Defaults to user_text, assistant_text, tool_input, and tool_error."},
			"tool_name": {"type": "string", "description": "Optional tool-name filter for tool_input, tool_error, or tool_output."},
			"limit": {"type": "integer", "description": "Maximum search hits to return, default 8, max 20."},
			"session_path": {"type": "string", "description": "Path from a search hit. Required for operation=around."},
			"message_index": {"type": "integer", "description": "Message index from a search hit. Required for operation=around."},
			"before": {"type": "integer", "description": "Messages before message_index for operation=around, default 3, max 10."},
			"after": {"type": "integer", "description": "Messages after message_index for operation=around, default 3, max 10."}
		},
		"required": ["operation"]
	}`
	if got := string(tl.Schema()); got != wantSchema {
		t.Fatalf("history schema changed; this is provider-visible and affects prompt-cache shape.\nwant:\n%s\n got:\n%s", wantSchema, got)
	}
}

func TestHistoryToolValidatesInputs(t *testing.T) {
	tl := NewTool(Options{SessionDir: t.TempDir()})
	for _, tc := range []struct {
		name string
		args string
	}{
		{"missing operation", `{}`},
		{"unknown operation", `{"operation":"scan"}`},
		{"around missing index", `{"operation":"around","session_path":"/tmp/session.jsonl"}`},
		{"bad json", `{"operation":`},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if _, err := tl.Execute(context.Background(), []byte(tc.args)); err == nil {
				t.Fatalf("Execute(%s) error = nil, want validation error", tc.args)
			}
		})
	}
}
