package mcp

import (
	"context"
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/yan1h/agent-coding-workflow/internal/codeindex"
	"github.com/yan1h/agent-coding-workflow/internal/projectmemory"
	"github.com/yan1h/agent-coding-workflow/internal/workspace"
)

// ===== fakes =====

type fakeCodeIndexSvc struct {
	snippets   []codeindex.Snippet
	lastWS     uuid.UUID
	lastQuery  codeindex.SearchQuery
	enqueuedWS uuid.UUID
}

func (f *fakeCodeIndexSvc) EnqueueBuild(_ context.Context, wsID uuid.UUID, _ *uuid.UUID, _ string) (*codeindex.Run, error) {
	f.enqueuedWS = wsID
	return &codeindex.Run{ID: uuid.New(), WorkspaceID: wsID}, nil
}
func (f *fakeCodeIndexSvc) Search(_ context.Context, wsID uuid.UUID, q codeindex.SearchQuery) ([]codeindex.Snippet, error) {
	f.lastWS = wsID
	f.lastQuery = q
	return f.snippets, nil
}
func (f *fakeCodeIndexSvc) MarkStale(context.Context, uuid.UUID) error { return nil }

type fakeMemorySvc struct {
	written  *projectmemory.WriteInput
	searched *projectmemory.Query
	entries  []projectmemory.Entry
}

func (f *fakeMemorySvc) Write(_ context.Context, in projectmemory.WriteInput) (*projectmemory.Entry, error) {
	f.written = &in
	return &projectmemory.Entry{ID: uuid.New(), ProjectID: in.ProjectID, Kind: in.Kind, Title: in.Title}, nil
}
func (f *fakeMemorySvc) Search(_ context.Context, q projectmemory.Query) ([]projectmemory.Entry, error) {
	f.searched = &q
	return f.entries, nil
}
func (f *fakeMemorySvc) List(_ context.Context, _ uuid.UUID, _ string) ([]projectmemory.Entry, error) {
	return f.entries, nil
}
func (f *fakeMemorySvc) Delete(context.Context, uuid.UUID) error { return nil }
func (f *fakeMemorySvc) RenderAgentsMD(context.Context, uuid.UUID, *uuid.UUID) (string, error) {
	return "", nil
}

// depsWithRAG builds testDeps plus a workspace + the code index/memory services.
func depsWithRAG(ci codeindex.Service, mem projectmemory.Service) ServerDeps {
	deps := testDeps()
	ws := &workspace.Workspace{
		ID: testWSID, ProjectID: testPID, Slug: "ws-1", Name: "WS",
		DefaultBranch: "main", SyncStatus: workspace.SyncStatusIdle,
	}
	deps.Workspaces = &fakeWorkspaceSvc{items: []*workspace.Workspace{ws}}
	deps.CodeIndex = ci
	deps.Memory = mem
	return deps
}

// ===== search_code =====

func TestSearchCode_HappyPath(t *testing.T) {
	ci := &fakeCodeIndexSvc{snippets: []codeindex.Snippet{
		{FilePath: "a.go", StartLine: 1, EndLine: 10, Content: "func A", Score: 0.9, CommitSHA: "abc"},
	}}
	deps := depsWithRAG(ci, &fakeMemorySvc{})
	result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
		"search_code", map[string]any{"workspace_id": testWSID.String(), "query": "find A", "top_k": 5})
	require.NoError(t, err)
	require.False(t, result.IsError)

	var out searchCodeOut
	unmarshalStructured(t, result, &out)
	require.Len(t, out.Results, 1)
	assert.Equal(t, "a.go", out.Results[0].File)
	assert.Equal(t, "find A", ci.lastQuery.Text)
	assert.Equal(t, 5, ci.lastQuery.TopK)
	assert.Equal(t, testWSID, ci.lastWS)
}

func TestSearchCode_CrossTenantRejected(t *testing.T) {
	deps := depsWithRAG(&fakeCodeIndexSvc{}, &fakeMemorySvc{})
	// Scope allows only a different project → project scope check must reject.
	result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{uuid.New()}}), deps,
		"search_code", map[string]any{"workspace_id": testWSID.String(), "query": "q"})
	if err == nil {
		require.True(t, result.IsError, "cross-tenant workspace must be rejected")
	}
}

func TestSearchCode_ToolScopeDenied(t *testing.T) {
	deps := depsWithRAG(&fakeCodeIndexSvc{}, &fakeMemorySvc{})
	// Token scope only allows memory_list → search_code denied.
	result, err := callTool(ctxWithAuth(Scope{Tools: []string{"memory_list"}}), deps,
		"search_code", map[string]any{"workspace_id": testWSID.String(), "query": "q"})
	if err == nil {
		require.True(t, result.IsError)
	}
}

func TestReindexWorkspace_HappyPath(t *testing.T) {
	ci := &fakeCodeIndexSvc{}
	deps := depsWithRAG(ci, &fakeMemorySvc{})
	result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
		"reindex_workspace", map[string]any{"workspace_id": testWSID.String()})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var out reindexWorkspaceOut
	unmarshalStructured(t, result, &out)
	assert.True(t, out.OK)
	assert.NotEmpty(t, out.RunID)
	assert.Equal(t, testWSID, ci.enqueuedWS)
}

// When CodeIndex is nil the tool is not registered → call errors (unknown tool).
func TestSearchCode_NotRegisteredWhenDisabled(t *testing.T) {
	deps := testDeps()
	deps.Memory = &fakeMemorySvc{} // memory present, code index nil
	result, err := callTool(ctxWithAuth(Scope{}), deps,
		"search_code", map[string]any{"workspace_id": testWSID.String(), "query": "q"})
	if err == nil {
		require.True(t, result.IsError, "disabled tool must not succeed")
	}
}

// ===== memory tools =====

func TestMemoryWrite_HappyPath(t *testing.T) {
	mem := &fakeMemorySvc{}
	deps := depsWithRAG(&fakeCodeIndexSvc{}, mem)
	result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
		"memory_write", map[string]any{
			"project_slug": "test-project", "kind": "decision",
			"title": "Use X", "body": "because Y", "tags": []string{"a", "b"},
		})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var out memoryWriteOut
	unmarshalStructured(t, result, &out)
	assert.True(t, out.OK)
	require.NotNil(t, mem.written)
	assert.Equal(t, testPID, mem.written.ProjectID)
	assert.Equal(t, "decision", mem.written.Kind)
	assert.Equal(t, []string{"a", "b"}, mem.written.Tags)
}

func TestMemoryWrite_ByWorkspace(t *testing.T) {
	mem := &fakeMemorySvc{}
	deps := depsWithRAG(&fakeCodeIndexSvc{}, mem)
	result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
		"memory_write", map[string]any{
			"workspace_id": testWSID.String(), "kind": "gotcha",
			"title": "T", "body": "B",
		})
	require.NoError(t, err)
	require.False(t, result.IsError)
	require.NotNil(t, mem.written)
	assert.Equal(t, testPID, mem.written.ProjectID)
	require.NotNil(t, mem.written.WorkspaceID)
	assert.Equal(t, testWSID, *mem.written.WorkspaceID)
}

func TestMemorySearch_HappyPath(t *testing.T) {
	mem := &fakeMemorySvc{entries: []projectmemory.Entry{
		{ID: uuid.New(), Kind: "decision", Title: "hit", Body: "x", Tags: []string{}},
	}}
	deps := depsWithRAG(&fakeCodeIndexSvc{}, mem)
	result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
		"memory_search", map[string]any{"project_slug": "test-project", "query": "hit"})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var out memorySearchOut
	unmarshalStructured(t, result, &out)
	require.Len(t, out.Results, 1)
	assert.Equal(t, "hit", out.Results[0].Title)
	require.NotNil(t, mem.searched)
	assert.Equal(t, testPID, mem.searched.ProjectID)
}

func TestMemoryList_HappyPath(t *testing.T) {
	mem := &fakeMemorySvc{entries: []projectmemory.Entry{
		{ID: uuid.New(), Kind: "convention", Title: "c", Tags: []string{}},
	}}
	deps := depsWithRAG(&fakeCodeIndexSvc{}, mem)
	result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{testPID}}), deps,
		"memory_list", map[string]any{"project_slug": "test-project"})
	require.NoError(t, err)
	require.False(t, result.IsError)
	var out memoryListOut
	unmarshalStructured(t, result, &out)
	require.Len(t, out.Entries, 1)
}

func TestMemoryWrite_CrossTenantRejected(t *testing.T) {
	deps := depsWithRAG(&fakeCodeIndexSvc{}, &fakeMemorySvc{})
	result, err := callTool(ctxWithAuth(Scope{ProjectIDs: []uuid.UUID{uuid.New()}}), deps,
		"memory_write", map[string]any{
			"project_slug": "test-project", "kind": "decision", "title": "T", "body": "B",
		})
	if err == nil {
		require.True(t, result.IsError, "cross-tenant project must be rejected")
	}
}

var (
	_ codeindex.Service     = (*fakeCodeIndexSvc)(nil)
	_ projectmemory.Service = (*fakeMemorySvc)(nil)
)
