package builtin

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

func TestCodeIndexSearchGoSymbols(t *testing.T) {
	root := t.TempDir()
	mkfile(t, filepath.Join(root, "service.go"), `package demo

type Service struct{}

func NewService() *Service { return &Service{} }

func (s *Service) Handle() {}

const DefaultName = "demo"
`)

	out := runTool(t, codeIndex{workDir: root}, map[string]any{
		"action": "search",
		"query":  "Service",
	})
	for _, want := range []string{
		"service.go:3: struct Service",
		"service.go:5: func NewService",
		"service.go:7: method Service.Handle",
	} {
		if !strings.Contains(out, want) {
			t.Fatalf("code_index search missing %q; got:\n%s", want, out)
		}
	}
}

func TestCodeIndexOutlineTypeScriptAndSkipsNoiseDirs(t *testing.T) {
	root := t.TempDir()
	mkfile(t, filepath.Join(root, "src", "app.ts"), `export interface User {
  name: string
}
export class Client {}
export const loadUser = async () => {}
`)
	mkfile(t, filepath.Join(root, "node_modules", "dep", "dep.ts"), `export class Dependency {}`)

	out := runTool(t, codeIndex{workDir: root}, map[string]any{
		"action": "outline",
		"path":   ".",
	})
	for _, want := range []string{
		"src/app.ts:1: interface User",
		"src/app.ts:4: class Client",
		"src/app.ts:5: func loadUser",
	} {
		if !strings.Contains(out, want) {
			t.Fatalf("code_index outline missing %q; got:\n%s", want, out)
		}
	}
	if strings.Contains(out, "Dependency") {
		t.Fatalf("code_index should skip node_modules; got:\n%s", out)
	}
}

func TestCodeIndexOutlineFiltersBeforeLimit(t *testing.T) {
	root := t.TempDir()
	mkfile(t, filepath.Join(root, "a.ts"), `export const first = () => {}
export const second = () => {}
`)
	mkfile(t, filepath.Join(root, "z.ts"), `export interface Later {
  id: string
}
`)

	out := runTool(t, codeIndex{workDir: root}, map[string]any{
		"action": "outline",
		"kind":   "interface",
		"limit":  1,
	})
	if !strings.Contains(out, "z.ts:1: interface Later") {
		t.Fatalf("code_index should find filtered symbols after early unfiltered matches; got:\n%s", out)
	}
	if strings.Contains(out, "first") || strings.Contains(out, "second") {
		t.Fatalf("code_index should filter before returning limited outline; got:\n%s", out)
	}
}

func TestCodeIndexKindFiltersJavaTypes(t *testing.T) {
	root := t.TempDir()
	mkfile(t, filepath.Join(root, "src", "Example.java"), `package demo;

public interface Repository {}
public enum Mode { Fast }
public class Client {
  public void run() {}
}
`)

	out := runTool(t, codeIndex{workDir: root}, map[string]any{
		"action": "search",
		"query":  "Repository",
		"kind":   "interface",
	})
	if !strings.Contains(out, "src/Example.java:3: interface Repository") {
		t.Fatalf("code_index should return Java interface for kind=interface; got:\n%s", out)
	}

	out = runTool(t, codeIndex{workDir: root}, map[string]any{
		"action": "search",
		"query":  "Mode",
		"kind":   "enum",
	})
	if !strings.Contains(out, "src/Example.java:4: enum Mode") {
		t.Fatalf("code_index should return Java enum for kind=enum; got:\n%s", out)
	}

	out = runTool(t, codeIndex{workDir: root}, map[string]any{
		"action": "search",
		"query":  "Client",
		"kind":   "class",
	})
	if !strings.Contains(out, "src/Example.java:5: class Client") {
		t.Fatalf("code_index should return Java class for kind=class; got:\n%s", out)
	}
}

func TestCodeIndexKindFiltersRustItems(t *testing.T) {
	root := t.TempDir()
	mkfile(t, filepath.Join(root, "src", "lib.rs"), `pub struct Store {}
pub trait Repository {}
pub enum Mode { Fast }
pub fn load_store() {}
`)

	out := runTool(t, codeIndex{workDir: root}, map[string]any{
		"action": "search",
		"query":  "Store",
		"kind":   "struct",
	})
	if !strings.Contains(out, "src/lib.rs:1: struct Store") {
		t.Fatalf("code_index should return Rust struct for kind=struct; got:\n%s", out)
	}

	out = runTool(t, codeIndex{workDir: root}, map[string]any{
		"action": "search",
		"query":  "Repository",
		"kind":   "trait",
	})
	if !strings.Contains(out, "src/lib.rs:2: trait Repository") {
		t.Fatalf("code_index should return Rust trait for kind=trait; got:\n%s", out)
	}

	out = runTool(t, codeIndex{workDir: root}, map[string]any{
		"action": "search",
		"query":  "load_store",
		"kind":   "fn",
	})
	if !strings.Contains(out, "src/lib.rs:4: fn load_store") {
		t.Fatalf("code_index should return Rust fn for kind=fn; got:\n%s", out)
	}
}

func TestCodeIndexRequiresQueryForSearch(t *testing.T) {
	_, err := codeIndex{workDir: t.TempDir()}.Execute(context.Background(), json.RawMessage(`{"action":"search"}`))
	if err == nil || !strings.Contains(err.Error(), "query is required") {
		t.Fatalf("error = %v, want query required", err)
	}
}

func TestCodeIndexWorkspaceBinding(t *testing.T) {
	root := t.TempDir()
	mkfile(t, filepath.Join(root, "main.py"), "class App:\n    pass\n")
	t.Chdir(t.TempDir())

	tools := byName(Workspace{Dir: root}.Tools("code_index"))
	tl := tools["code_index"]
	if tl == nil {
		t.Fatal("workspace did not expose code_index")
	}
	out, err := tl.Execute(context.Background(), json.RawMessage(`{"action":"outline","path":"."}`))
	if err != nil {
		t.Fatalf("code_index: %v", err)
	}
	if !strings.Contains(out, "main.py:1: class App") {
		t.Fatalf("code_index should resolve relative paths against workspace; got:\n%s", out)
	}
}

func TestCodeIndexForbidRead(t *testing.T) {
	root := t.TempDir()
	mkfile(t, filepath.Join(root, "allowed.go"), "package demo\nfunc AllowedNeedle() {}\n")
	secretDir := filepath.Join(root, "secret")
	mkfile(t, filepath.Join(secretDir, "secret.go"), "package secret\nfunc SecretNeedle() {}\n")

	ci := codeIndex{workDir: root, forbidRoots: realRoots([]string{secretDir})}
	out := runTool(t, ci, map[string]any{
		"action": "search",
		"query":  "Needle",
		"path":   ".",
	})
	if !strings.Contains(out, "AllowedNeedle") {
		t.Fatalf("code_index should still return allowed symbols, got:\n%s", out)
	}
	if strings.Contains(out, "SecretNeedle") || strings.Contains(out, "secret.go") {
		t.Fatalf("code_index leaked forbidden symbols:\n%s", out)
	}

	out = runTool(t, ci, map[string]any{
		"action": "search",
		"query":  "SecretNeedle",
		"path":   "secret",
	})
	if out != "(no symbols)" {
		t.Fatalf("code_index forbidden root = %q, want (no symbols)", out)
	}
}

func TestCodeIndexIgnoresLargeFiles(t *testing.T) {
	root := t.TempDir()
	if err := os.WriteFile(filepath.Join(root, "huge.ts"), []byte(strings.Repeat("x", codeIndexMaxFileSize+1)), 0o644); err != nil {
		t.Fatal(err)
	}
	out := runTool(t, codeIndex{workDir: root}, map[string]any{"action": "outline"})
	if out != "(no symbols)" {
		t.Fatalf("large files should be ignored, got:\n%s", out)
	}
}
