package github

import (
	"context"
	"encoding/json"
	"net/http"
	"strings"
	"testing"

	"github.com/github/github-mcp-server/internal/githubv4mock"
	"github.com/github/github-mcp-server/internal/toolsnaps"
	"github.com/github/github-mcp-server/pkg/http/headers"
	transportpkg "github.com/github/github-mcp-server/pkg/http/transport"
	"github.com/github/github-mcp-server/pkg/inventory"
	"github.com/github/github-mcp-server/pkg/translations"
	gogithub "github.com/google/go-github/v89/github"
	"github.com/shurcooL/githubv4"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func granularToolsForToolset(toolsetID inventory.ToolsetID, featureFlag string) []inventory.ServerTool {
	var result []inventory.ServerTool
	for _, tool := range AllTools(translations.NullTranslationHelper) {
		if tool.Toolset.ID == toolsetID && tool.FeatureFlagEnable == featureFlag {
			result = append(result, tool)
		}
	}
	return result
}

func TestGranularToolSnaps(t *testing.T) {
	// Test toolsnaps for all granular tools
	toolConstructors := []func(translations.TranslationHelperFunc) inventory.ServerTool{
		GranularCreateIssue,
		GranularUpdateIssueTitle,
		GranularUpdateIssueBody,
		GranularUpdateIssueAssignees,
		GranularUpdateIssueLabels,
		GranularUpdateIssueMilestone,
		GranularUpdateIssueType,
		GranularUpdateIssueState,
		GranularAddSubIssue,
		GranularRemoveSubIssue,
		GranularReprioritizeSubIssue,
		GranularSetIssueFields,
		GranularAddIssueReaction,
		GranularAddIssueCommentReaction,
		GranularUpdatePullRequestTitle,
		GranularUpdatePullRequestBody,
		GranularUpdatePullRequestState,
		GranularUpdatePullRequestDraftState,
		GranularRequestPullRequestReviewers,
		GranularCreatePullRequestReview,
		GranularSubmitPendingPullRequestReview,
		GranularDeletePendingPullRequestReview,
		GranularAddPullRequestReviewComment,
		GranularResolveReviewThread,
		GranularUnresolveReviewThread,
		GranularAddPullRequestReviewCommentReaction,
	}

	for _, constructor := range toolConstructors {
		serverTool := constructor(translations.NullTranslationHelper)
		t.Run(serverTool.Tool.Name, func(t *testing.T) {
			require.NoError(t, toolsnaps.Test(serverTool.Tool.Name, serverTool.Tool))
		})
	}
}

func TestIssuesGranularToolset(t *testing.T) {
	t.Run("toolset contains expected granular tools", func(t *testing.T) {
		tools := granularToolsForToolset(ToolsetMetadataIssues.ID, FeatureFlagIssuesGranular)

		toolNames := make([]string, 0, len(tools))
		for _, tool := range tools {
			toolNames = append(toolNames, tool.Tool.Name)
		}

		expected := []string{
			"create_issue",
			"update_issue_title",
			"update_issue_body",
			"update_issue_assignees",
			"update_issue_labels",
			"update_issue_milestone",
			"update_issue_type",
			"update_issue_state",
			"add_sub_issue",
			"remove_sub_issue",
			"reprioritize_sub_issue",
			"set_issue_fields",
			"add_issue_reaction",
			"add_issue_comment_reaction",
		}
		for _, name := range expected {
			assert.Contains(t, toolNames, name)
		}
		assert.Len(t, tools, len(expected))
	})

	t.Run("all granular tools have correct feature flag", func(t *testing.T) {
		for _, tool := range granularToolsForToolset(ToolsetMetadataIssues.ID, FeatureFlagIssuesGranular) {
			assert.Equal(t, FeatureFlagIssuesGranular, tool.FeatureFlagEnable, "tool %s", tool.Tool.Name)
		}
	})
}

func TestPullRequestsGranularToolset(t *testing.T) {
	t.Run("toolset contains expected granular tools", func(t *testing.T) {
		tools := granularToolsForToolset(ToolsetMetadataPullRequests.ID, FeatureFlagPullRequestsGranular)

		toolNames := make([]string, 0, len(tools))
		for _, tool := range tools {
			toolNames = append(toolNames, tool.Tool.Name)
		}

		expected := []string{
			"update_pull_request_title",
			"update_pull_request_body",
			"update_pull_request_state",
			"update_pull_request_draft_state",
			"request_pull_request_reviewers",
			"create_pull_request_review",
			"submit_pending_pull_request_review",
			"delete_pending_pull_request_review",
			"add_pull_request_review_comment",
			"resolve_review_thread",
			"unresolve_review_thread",
			"add_pull_request_review_comment_reaction",
		}
		for _, name := range expected {
			assert.Contains(t, toolNames, name)
		}
		assert.Len(t, tools, len(expected))
	})

	t.Run("all granular tools have correct feature flag", func(t *testing.T) {
		for _, tool := range granularToolsForToolset(ToolsetMetadataPullRequests.ID, FeatureFlagPullRequestsGranular) {
			assert.Equal(t, FeatureFlagPullRequestsGranular, tool.FeatureFlagEnable, "tool %s", tool.Tool.Name)
		}
	})
}

// --- Issue granular tool handler tests ---

func TestGranularCreateIssue(t *testing.T) {
	mockIssue := &gogithub.Issue{
		Number: gogithub.Ptr(1),
		Title:  gogithub.Ptr("Test Issue"),
		Body:   gogithub.Ptr("Test body"),
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectedErrMsg string
	}{
		{
			name: "successful creation",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesByOwnerByRepo: expectRequestBody(t, map[string]any{
					"title": "Test Issue",
					"body":  "Test body",
				}).andThen(mockResponse(t, http.StatusCreated, mockIssue)),
			}),
			requestArgs: map[string]any{
				"owner": "owner",
				"repo":  "repo",
				"title": "Test Issue",
				"body":  "Test body",
			},
		},
		{
			name:         "missing required parameter",
			mockedClient: MockHTTPClientWithHandlers(nil),
			requestArgs: map[string]any{
				"owner": "owner",
				"repo":  "repo",
			},
			expectedErrMsg: "missing required parameter: title",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, tc.mockedClient)
			deps := BaseDeps{Client: client}
			serverTool := GranularCreateIssue(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)

			if tc.expectedErrMsg != "" {
				textContent := getTextResult(t, result)
				assert.Contains(t, textContent.Text, tc.expectedErrMsg)
				return
			}
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularUpdateIssueTitle(t *testing.T) {
	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		PatchReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, &gogithub.Issue{
			Number: gogithub.Ptr(42),
			Title:  gogithub.Ptr("New Title"),
		}),
	}))
	deps := BaseDeps{Client: client}
	serverTool := GranularUpdateIssueTitle(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"owner":        "owner",
		"repo":         "repo",
		"issue_number": float64(42),
		"title":        "New Title",
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularUpdateIssueBody(t *testing.T) {
	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
			"body": "Updated body",
		}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{
			Number: gogithub.Ptr(1),
			Body:   gogithub.Ptr("Updated body"),
		})),
	}))
	deps := BaseDeps{Client: client}
	serverTool := GranularUpdateIssueBody(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"owner":        "owner",
		"repo":         "repo",
		"issue_number": float64(1),
		"body":         "Updated body",
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularUpdateIssueAssignees(t *testing.T) {
	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
			"assignees": []any{"user1", "user2"},
		}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
	}))
	deps := BaseDeps{Client: client}
	serverTool := GranularUpdateIssueAssignees(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"owner":        "owner",
		"repo":         "repo",
		"issue_number": float64(1),
		"assignees":    []string{"user1", "user2"},
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularUpdateIssueAssigneesObjectForm(t *testing.T) {
	tests := []struct {
		name        string
		requestArgs map[string]any
		expectedReq map[string]any
	}{
		{
			name: "assignee objects without intent serialize as strings",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"assignees": []any{
					map[string]any{"login": "octocat"},
					"monalisa",
				},
			},
			expectedReq: map[string]any{
				"assignees": []any{"octocat", "monalisa"},
			},
		},
		{
			name: "assignee suggested without rationale",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"assignees": []any{
					map[string]any{"login": "octocat", "is_suggestion": true},
				},
			},
			expectedReq: map[string]any{
				"assignees": []any{
					map[string]any{"login": "octocat", "suggest": true},
				},
			},
		},
		{
			name: "suggested assignee with rationale and confidence",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"assignees": []any{
					map[string]any{"login": "octocat", "rationale": "  Authored the crashing file  ", "confidence": "high", "is_suggestion": true},
				},
			},
			expectedReq: map[string]any{
				"assignees": []any{
					map[string]any{"login": "octocat", "rationale": "Authored the crashing file", "confidence": "HIGH", "suggest": true},
				},
			},
		},
		{
			name: "mix of plain and suggested assignees",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"assignees": []any{
					"monalisa",
					map[string]any{"login": "octocat", "is_suggestion": true},
				},
			},
			expectedReq: map[string]any{
				"assignees": []any{
					"monalisa",
					map[string]any{"login": "octocat", "suggest": true},
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
					andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
			}))
			deps := BaseDeps{Client: client}
			serverTool := GranularUpdateIssueAssignees(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularUpdateIssueAssigneesInvalidInput(t *testing.T) {
	tests := []struct {
		name            string
		requestArgs     map[string]any
		expectedErrText string
	}{
		{
			name: "rationale too long",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"assignees": []any{
					map[string]any{"login": "octocat", "rationale": strings.Repeat("a", 281)},
				},
			},
			expectedErrText: "assignee rationale must be 280 characters or less",
		},
		{
			name: "assignee object missing login",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"assignees": []any{
					map[string]any{"rationale": "no login provided"},
				},
			},
			expectedErrText: "each assignee object must have a 'login' string",
		},
		{
			name: "invalid confidence value",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"assignees": []any{
					map[string]any{"login": "octocat", "confidence": "maybe"},
				},
			},
			expectedErrText: "confidence must be one of: LOW, MEDIUM, HIGH",
		},
		{
			name: "assignee entry is neither string nor object",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"assignees":    []any{float64(123)},
			},
			expectedErrText: "each assignee must be a string or an object",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
			serverTool := GranularUpdateIssueAssignees(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)

			errorContent := getErrorResult(t, result)
			assert.Contains(t, errorContent.Text, tc.expectedErrText)
		})
	}
}

func TestGranularUpdateIssueLabels(t *testing.T) {
	tests := []struct {
		name        string
		requestArgs map[string]any
		expectedReq map[string]any
	}{
		{
			name: "labels as plain strings",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels":       []any{"bug", "enhancement"},
			},
			expectedReq: map[string]any{
				"labels": []any{"bug", "enhancement"},
			},
		},
		{
			name: "label objects without rationale serialize as strings",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels": []any{
					map[string]any{"name": "bug"},
					"enhancement",
				},
			},
			expectedReq: map[string]any{
				"labels": []any{"bug", "enhancement"},
			},
		},
		{
			name: "mixed strings and label objects with rationale",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels": []any{
					"triage",
					map[string]any{"name": "bug", "rationale": "  Reports a crash when saving  "},
					map[string]any{"name": "frontend", "rationale": "Mentions the UI button"},
				},
			},
			expectedReq: map[string]any{
				"labels": []any{
					"triage",
					map[string]any{"name": "bug", "rationale": "Reports a crash when saving"},
					map[string]any{"name": "frontend", "rationale": "Mentions the UI button"},
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
					andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
			}))
			deps := BaseDeps{Client: client}
			serverTool := GranularUpdateIssueLabels(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularUpdateIssueLabelsSuggest(t *testing.T) {
	tests := []struct {
		name        string
		requestArgs map[string]any
		expectedReq map[string]any
	}{
		{
			name: "single label suggested without rationale",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels": []any{
					map[string]any{"name": "bug", "is_suggestion": true},
				},
			},
			expectedReq: map[string]any{
				"labels": []any{
					map[string]any{"name": "bug", "suggest": true},
				},
			},
		},
		{
			name: "suggested label with rationale",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels": []any{
					map[string]any{"name": "frontend", "rationale": "Mentions the UI button", "is_suggestion": true},
				},
			},
			expectedReq: map[string]any{
				"labels": []any{
					map[string]any{"name": "frontend", "rationale": "Mentions the UI button", "suggest": true},
				},
			},
		},
		{
			name: "mix of plain, applied-with-rationale, and suggested labels",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels": []any{
					"triage",
					map[string]any{"name": "bug", "rationale": "Reports a crash when saving"},
					map[string]any{"name": "needs-design", "is_suggestion": true},
				},
			},
			expectedReq: map[string]any{
				"labels": []any{
					"triage",
					map[string]any{"name": "bug", "rationale": "Reports a crash when saving"},
					map[string]any{"name": "needs-design", "suggest": true},
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
					andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
			}))
			deps := BaseDeps{Client: client}
			serverTool := GranularUpdateIssueLabels(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularUpdateIssueLabelsInvalidRationale(t *testing.T) {
	tests := []struct {
		name            string
		requestArgs     map[string]any
		expectedErrText string
	}{
		{
			name: "rationale too long",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels": []any{
					map[string]any{"name": "bug", "rationale": strings.Repeat("a", 281)},
				},
			},
			expectedErrText: "label rationale must be 280 characters or less",
		},
		{
			name: "label object missing name",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels": []any{
					map[string]any{"rationale": "no name provided"},
				},
			},
			expectedErrText: "each label object must have a 'name' string",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
			serverTool := GranularUpdateIssueLabels(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)

			errorContent := getErrorResult(t, result)
			assert.Contains(t, errorContent.Text, tc.expectedErrText)
		})
	}
}

func TestGranularUpdateIssueLabelsConfidence(t *testing.T) {
	tests := []struct {
		name        string
		requestArgs map[string]any
		expectedReq map[string]any
	}{
		{
			name: "label with confidence triggers object form",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels": []any{
					map[string]any{"name": "bug", "confidence": "HIGH"},
				},
			},
			expectedReq: map[string]any{
				"labels": []any{
					map[string]any{"name": "bug", "confidence": "HIGH"},
				},
			},
		},
		{
			name: "label with confidence and rationale",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels": []any{
					map[string]any{"name": "bug", "rationale": "Reports a crash", "confidence": "MEDIUM"},
				},
			},
			expectedReq: map[string]any{
				"labels": []any{
					map[string]any{"name": "bug", "rationale": "Reports a crash", "confidence": "MEDIUM"},
				},
			},
		},
		{
			name: "label confidence is normalized",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels": []any{
					map[string]any{"name": "bug", "confidence": " high\t"},
				},
			},
			expectedReq: map[string]any{
				"labels": []any{
					map[string]any{"name": "bug", "confidence": "HIGH"},
				},
			},
		},
		{
			name: "invalid confidence value",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"labels": []any{
					map[string]any{"name": "bug", "confidence": "very_high"},
				},
			},
			expectedReq: nil,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			if tc.expectedReq == nil {
				// Error case
				deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
				serverTool := GranularUpdateIssueLabels(translations.NullTranslationHelper)
				handler := serverTool.Handler(deps)

				request := createMCPRequest(tc.requestArgs)
				result, err := handler(ContextWithDeps(context.Background(), deps), &request)
				require.NoError(t, err)

				errorContent := getErrorResult(t, result)
				assert.Contains(t, errorContent.Text, "confidence must be one of: LOW, MEDIUM, HIGH")
				return
			}

			client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
					andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
			}))
			deps := BaseDeps{Client: client}
			serverTool := GranularUpdateIssueLabels(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularUpdateIssueMilestone(t *testing.T) {
	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
			"milestone": float64(5),
		}).andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
	}))
	deps := BaseDeps{Client: client}
	serverTool := GranularUpdateIssueMilestone(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"owner":        "owner",
		"repo":         "repo",
		"issue_number": float64(1),
		"milestone":    float64(5),
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularUpdateIssueType(t *testing.T) {
	tests := []struct {
		name        string
		requestArgs map[string]any
		expectedReq map[string]any
	}{
		{
			name: "type only",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"issue_type":   "bug",
			},
			expectedReq: map[string]any{
				"type": "bug",
			},
		},
		{
			name: "type with rationale",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"issue_type":   "feature",
				"rationale":    "  This issue requests a new capability  ",
			},
			expectedReq: map[string]any{
				"type": map[string]any{
					"value":     "feature",
					"rationale": "This issue requests a new capability",
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
					andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
			}))
			deps := BaseDeps{Client: client}
			serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularUpdateIssueTypeSuggest(t *testing.T) {
	tests := []struct {
		name        string
		requestArgs map[string]any
		expectedReq map[string]any
	}{
		{
			name: "suggest without rationale",
			requestArgs: map[string]any{
				"owner":         "owner",
				"repo":          "repo",
				"issue_number":  float64(1),
				"issue_type":    "bug",
				"is_suggestion": true,
			},
			expectedReq: map[string]any{
				"type": map[string]any{
					"value":   "bug",
					"suggest": true,
				},
			},
		},
		{
			name: "suggest with rationale",
			requestArgs: map[string]any{
				"owner":         "owner",
				"repo":          "repo",
				"issue_number":  float64(1),
				"issue_type":    "feature",
				"rationale":     "  Asks for dark mode support  ",
				"is_suggestion": true,
			},
			expectedReq: map[string]any{
				"type": map[string]any{
					"value":     "feature",
					"rationale": "Asks for dark mode support",
					"suggest":   true,
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
					andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
			}))
			deps := BaseDeps{Client: client}
			serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularUpdateIssueTypeInvalidRationale(t *testing.T) {
	tests := []struct {
		name            string
		requestArgs     map[string]any
		expectedErrText string
	}{
		{
			name: "rationale wrong type",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"issue_type":   "feature",
				"rationale":    float64(123),
			},
			expectedErrText: "parameter rationale is not of type string, is float64",
		},
		{
			name: "rationale too long",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"issue_type":   "feature",
				"rationale":    strings.Repeat("a", 281),
			},
			expectedErrText: "parameter rationale must be 280 characters or less",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
			serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)

			errorContent := getErrorResult(t, result)
			assert.Contains(t, errorContent.Text, tc.expectedErrText)
		})
	}
}

func TestGranularUpdateIssueTypeConfidence(t *testing.T) {
	tests := []struct {
		name        string
		requestArgs map[string]any
		expectedReq map[string]any
	}{
		{
			name: "type with confidence only",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"issue_type":   "bug",
				"confidence":   "HIGH",
			},
			expectedReq: map[string]any{
				"type": map[string]any{
					"value":      "bug",
					"confidence": "HIGH",
				},
			},
		},
		{
			name: "type with confidence and rationale",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"issue_type":   "feature",
				"rationale":    "Asks for dark mode support",
				"confidence":   "MEDIUM",
			},
			expectedReq: map[string]any{
				"type": map[string]any{
					"value":      "feature",
					"rationale":  "Asks for dark mode support",
					"confidence": "MEDIUM",
				},
			},
		},
		{
			name: "type with low confidence triggers object form",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"issue_type":   "bug",
				"confidence":   "LOW",
			},
			expectedReq: map[string]any{
				"type": map[string]any{
					"value":      "bug",
					"confidence": "LOW",
				},
			},
		},
		{
			name: "type confidence is normalized",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"issue_type":   "bug",
				"confidence":   " medium ",
			},
			expectedReq: map[string]any{
				"type": map[string]any{
					"value":      "bug",
					"confidence": "MEDIUM",
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
					andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
			}))
			deps := BaseDeps{Client: client}
			serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularUpdateIssueTypeInvalidConfidence(t *testing.T) {
	tests := []struct {
		name            string
		requestArgs     map[string]any
		expectedErrText string
	}{
		{
			name: "invalid confidence value",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"issue_type":   "bug",
				"confidence":   "very_high",
			},
			expectedErrText: "confidence must be one of: LOW, MEDIUM, HIGH",
		},
		{
			name: "confidence wrong type",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"issue_type":   "bug",
				"confidence":   float64(85),
			},
			expectedErrText: "parameter confidence is not of type string",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
			serverTool := GranularUpdateIssueType(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)

			errorContent := getErrorResult(t, result)
			assert.Contains(t, errorContent.Text, tc.expectedErrText)
		})
	}
}

func TestGranularUpdateIssueState(t *testing.T) {
	tests := []struct {
		name        string
		requestArgs map[string]any
		expectedReq map[string]any
	}{
		{
			name: "close with reason",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"state":        "closed",
				"state_reason": "completed",
			},
			expectedReq: map[string]any{
				"state":        "closed",
				"state_reason": "completed",
			},
		},
		{
			name: "reopen without reason",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"state":        "open",
			},
			expectedReq: map[string]any{
				"state": "open",
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
					andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{
						Number: gogithub.Ptr(1),
						State:  gogithub.Ptr(tc.requestArgs["state"].(string)),
					})),
			}))
			deps := BaseDeps{Client: client}
			serverTool := GranularUpdateIssueState(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularUpdateIssueStateSuggest(t *testing.T) {
	tests := []struct {
		name        string
		requestArgs map[string]any
		expectedReq map[string]any
	}{
		{
			name: "suggest without rationale",
			requestArgs: map[string]any{
				"owner":         "owner",
				"repo":          "repo",
				"issue_number":  float64(1),
				"state":         "closed",
				"is_suggestion": true,
			},
			expectedReq: map[string]any{
				"state": map[string]any{
					"value":   "closed",
					"suggest": true,
				},
			},
		},
		{
			name: "suggest with rationale and state_reason",
			requestArgs: map[string]any{
				"owner":         "owner",
				"repo":          "repo",
				"issue_number":  float64(1),
				"state":         "closed",
				"state_reason":  "not_planned",
				"rationale":     "  No activity in 6 months  ",
				"is_suggestion": true,
			},
			expectedReq: map[string]any{
				"state": map[string]any{
					"value":     "closed",
					"rationale": "No activity in 6 months",
					"suggest":   true,
				},
				"state_reason": "not_planned",
			},
		},
		{
			name: "rationale applied directly (no suggestion)",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"state":        "closed",
				"rationale":    "The reported crash is fixed in v2.1",
				"confidence":   "HIGH",
			},
			expectedReq: map[string]any{
				"state": map[string]any{
					"value":      "closed",
					"rationale":  "The reported crash is fixed in v2.1",
					"confidence": "HIGH",
				},
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
					andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
			}))
			deps := BaseDeps{Client: client}
			serverTool := GranularUpdateIssueState(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularUpdateIssueStateDuplicate(t *testing.T) {
	const duplicateIssueID = int64(99999)

	tests := []struct {
		name        string
		requestArgs map[string]any
		expectedReq map[string]any
	}{
		{
			name: "suggestion duplicate close",
			requestArgs: map[string]any{
				"owner":         "owner",
				"repo":          "repo",
				"issue_number":  float64(1),
				"state":         "closed",
				"state_reason":  "duplicate",
				"is_suggestion": true,
				"duplicate_of":  float64(42),
			},
			expectedReq: map[string]any{
				"state": map[string]any{
					"value":   "closed",
					"suggest": true,
				},
				"state_reason":       "duplicate",
				"duplicate_issue_id": float64(duplicateIssueID),
			},
		},
		{
			name: "direct duplicate close",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"state":        "closed",
				"state_reason": "duplicate",
				"duplicate_of": float64(42),
			},
			expectedReq: map[string]any{
				"state":              map[string]any{"value": "closed"},
				"state_reason":       "duplicate",
				"duplicate_issue_id": float64(duplicateIssueID),
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, &gogithub.Issue{
					ID:     gogithub.Ptr(duplicateIssueID),
					Number: gogithub.Ptr(42),
				}),
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
					andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
			}))
			deps := BaseDeps{Client: client}
			serverTool := GranularUpdateIssueState(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularUpdateIssueStateInvalidRationale(t *testing.T) {
	tests := []struct {
		name            string
		requestArgs     map[string]any
		expectedErrText string
	}{
		{
			name: "rationale too long",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"state":        "closed",
				"rationale":    strings.Repeat("a", 281),
			},
			expectedErrText: "parameter rationale must be 280 characters or less",
		},
		{
			name: "duplicate_of without state_reason duplicate",
			requestArgs: map[string]any{
				"owner":         "owner",
				"repo":          "repo",
				"issue_number":  float64(1),
				"state":         "closed",
				"state_reason":  "not_planned",
				"is_suggestion": true,
				"duplicate_of":  float64(42),
			},
			expectedErrText: "duplicate_of can only be used when state_reason is 'duplicate'",
		},
		{
			name: "suggestion duplicate without duplicate_of",
			requestArgs: map[string]any{
				"owner":         "owner",
				"repo":          "repo",
				"issue_number":  float64(1),
				"state":         "closed",
				"state_reason":  "duplicate",
				"is_suggestion": true,
			},
			expectedErrText: "duplicate_of is required when suggesting a close as duplicate",
		},
		{
			name: "state_reason with open state",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(1),
				"state":        "open",
				"state_reason": "completed",
			},
			expectedErrText: "state_reason can only be used when state is 'closed'",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
			serverTool := GranularUpdateIssueState(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(tc.requestArgs)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)

			errorContent := getErrorResult(t, result)
			assert.Contains(t, errorContent.Text, tc.expectedErrText)
		})
	}
}

func TestGranularUpdateIssueStateInvalidConfidence(t *testing.T) {
	deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
	serverTool := GranularUpdateIssueState(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"owner":        "owner",
		"repo":         "repo",
		"issue_number": float64(1),
		"state":        "closed",
		"confidence":   "VERY_HIGH",
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)

	errorContent := getErrorResult(t, result)
	assert.Contains(t, errorContent.Text, "confidence must be one of: LOW, MEDIUM, HIGH")
}

// --- Pull request granular tool handler tests ---

func TestGranularUpdatePullRequestTitle(t *testing.T) {
	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{
			"title": "New PR Title",
		}).andThen(mockResponse(t, http.StatusOK, &gogithub.PullRequest{
			Number: gogithub.Ptr(1),
			Title:  gogithub.Ptr("New PR Title"),
		})),
	}))
	deps := BaseDeps{Client: client}
	serverTool := GranularUpdatePullRequestTitle(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"owner":      "owner",
		"repo":       "repo",
		"pullNumber": float64(1),
		"title":      "New PR Title",
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularUpdatePullRequestBody(t *testing.T) {
	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{
			"body": "Updated description",
		}).andThen(mockResponse(t, http.StatusOK, &gogithub.PullRequest{
			Number: gogithub.Ptr(1),
			Body:   gogithub.Ptr("Updated description"),
		})),
	}))
	deps := BaseDeps{Client: client}
	serverTool := GranularUpdatePullRequestBody(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"owner":      "owner",
		"repo":       "repo",
		"pullNumber": float64(1),
		"body":       "Updated description",
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularUpdatePullRequestState(t *testing.T) {
	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		PatchReposPullsByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{
			"state": "closed",
		}).andThen(mockResponse(t, http.StatusOK, &gogithub.PullRequest{
			Number: gogithub.Ptr(1),
			State:  gogithub.Ptr("closed"),
		})),
	}))
	deps := BaseDeps{Client: client}
	serverTool := GranularUpdatePullRequestState(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"owner":      "owner",
		"repo":       "repo",
		"pullNumber": float64(1),
		"state":      "closed",
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularRequestPullRequestReviewers(t *testing.T) {
	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		PostReposPullsRequestedReviewersByOwnerByRepoByPullNumber: expectRequestBody(t, map[string]any{
			"reviewers":      []any{"user1"},
			"team_reviewers": []any{"team1"},
		}).andThen(mockResponse(t, http.StatusOK, &gogithub.PullRequest{Number: gogithub.Ptr(1)})),
	}))
	deps := BaseDeps{Client: client}
	serverTool := GranularRequestPullRequestReviewers(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"owner":      "owner",
		"repo":       "repo",
		"pullNumber": float64(1),
		"reviewers":  []string{"user1", "owner/team1"},
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularCreatePullRequestReview(t *testing.T) {
	mockedClient := githubv4mock.NewMockedHTTPClient(
		githubv4mock.NewQueryMatcher(
			struct {
				Repository struct {
					PullRequest struct {
						ID githubv4.ID
					} `graphql:"pullRequest(number: $prNum)"`
				} `graphql:"repository(owner: $owner, name: $repo)"`
			}{},
			map[string]any{
				"owner": githubv4.String("owner"),
				"repo":  githubv4.String("repo"),
				"prNum": githubv4.Int(1),
			},
			githubv4mock.DataResponse(map[string]any{
				"repository": map[string]any{
					"pullRequest": map[string]any{
						"id": "PR_123",
					},
				},
			}),
		),
		githubv4mock.NewMutationMatcher(
			struct {
				AddPullRequestReview struct {
					PullRequestReview struct {
						ID githubv4.ID
					}
				} `graphql:"addPullRequestReview(input: $input)"`
			}{},
			githubv4.AddPullRequestReviewInput{
				PullRequestID: githubv4.ID("PR_123"),
				Body:          githubv4.NewString("LGTM"),
				Event:         githubv4mock.Ptr(githubv4.PullRequestReviewEventApprove),
			},
			nil,
			githubv4mock.DataResponse(map[string]any{}),
		),
	)
	gqlClient := githubv4.NewClient(mockedClient)
	deps := BaseDeps{GQLClient: gqlClient}
	serverTool := GranularCreatePullRequestReview(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"owner":      "owner",
		"repo":       "repo",
		"pullNumber": float64(1),
		"body":       "LGTM",
		"event":      "APPROVE",
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularUpdatePullRequestDraftState(t *testing.T) {
	tests := []struct {
		name  string
		draft bool
	}{
		{name: "convert to draft", draft: true},
		{name: "mark ready for review", draft: false},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			var matchers []githubv4mock.Matcher

			matchers = append(matchers, githubv4mock.NewQueryMatcher(
				struct {
					Repository struct {
						PullRequest struct {
							ID githubv4.ID
						} `graphql:"pullRequest(number: $number)"`
					} `graphql:"repository(owner: $owner, name: $name)"`
				}{},
				map[string]any{
					"owner":  githubv4.String("owner"),
					"name":   githubv4.String("repo"),
					"number": githubv4.Int(1),
				},
				githubv4mock.DataResponse(map[string]any{
					"repository": map[string]any{
						"pullRequest": map[string]any{"id": "PR_123"},
					},
				}),
			))

			if tc.draft {
				matchers = append(matchers, githubv4mock.NewMutationMatcher(
					struct {
						ConvertPullRequestToDraft struct {
							PullRequest struct {
								ID      githubv4.ID
								IsDraft githubv4.Boolean
							}
						} `graphql:"convertPullRequestToDraft(input: $input)"`
					}{},
					githubv4.ConvertPullRequestToDraftInput{PullRequestID: githubv4.ID("PR_123")},
					nil,
					githubv4mock.DataResponse(map[string]any{
						"convertPullRequestToDraft": map[string]any{
							"pullRequest": map[string]any{"id": "PR_123", "isDraft": true},
						},
					}),
				))
			} else {
				matchers = append(matchers, githubv4mock.NewMutationMatcher(
					struct {
						MarkPullRequestReadyForReview struct {
							PullRequest struct {
								ID      githubv4.ID
								IsDraft githubv4.Boolean
							}
						} `graphql:"markPullRequestReadyForReview(input: $input)"`
					}{},
					githubv4.MarkPullRequestReadyForReviewInput{PullRequestID: githubv4.ID("PR_123")},
					nil,
					githubv4mock.DataResponse(map[string]any{
						"markPullRequestReadyForReview": map[string]any{
							"pullRequest": map[string]any{"id": "PR_123", "isDraft": false},
						},
					}),
				))
			}

			gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...))
			deps := BaseDeps{GQLClient: gqlClient}
			serverTool := GranularUpdatePullRequestDraftState(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(map[string]any{
				"owner":      "owner",
				"repo":       "repo",
				"pullNumber": float64(1),
				"draft":      tc.draft,
			})
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			assert.False(t, result.IsError)
		})
	}
}

func TestGranularAddPullRequestReviewComment(t *testing.T) {
	mockedClient := githubv4mock.NewMockedHTTPClient(
		githubv4mock.NewQueryMatcher(
			struct {
				Viewer struct {
					Login githubv4.String
				}
			}{},
			nil,
			githubv4mock.DataResponse(map[string]any{
				"viewer": map[string]any{"login": "testuser"},
			}),
		),
		githubv4mock.NewQueryMatcher(
			struct {
				Repository struct {
					PullRequest struct {
						Reviews struct {
							Nodes []struct {
								ID    githubv4.ID
								State githubv4.PullRequestReviewState
								URL   githubv4.URI
							}
						} `graphql:"reviews(first: 1, author: $author)"`
					} `graphql:"pullRequest(number: $prNum)"`
				} `graphql:"repository(owner: $owner, name: $name)"`
			}{},
			map[string]any{
				"author": githubv4.String("testuser"),
				"owner":  githubv4.String("owner"),
				"name":   githubv4.String("repo"),
				"prNum":  githubv4.Int(1),
			},
			githubv4mock.DataResponse(map[string]any{
				"repository": map[string]any{
					"pullRequest": map[string]any{
						"reviews": map[string]any{
							"nodes": []map[string]any{
								{"id": "PRR_123", "state": "PENDING", "url": "https://github.com/owner/repo/pull/1#pullrequestreview-123"},
							},
						},
					},
				},
			}),
		),
		githubv4mock.NewMutationMatcher(
			struct {
				AddPullRequestReviewThread struct {
					Thread struct {
						ID githubv4.ID
					}
				} `graphql:"addPullRequestReviewThread(input: $input)"`
			}{},
			githubv4.AddPullRequestReviewThreadInput{
				Path:                githubv4.String("src/main.go"),
				Body:                githubv4.String("This needs a fix"),
				SubjectType:         githubv4mock.Ptr(githubv4.PullRequestReviewThreadSubjectTypeLine),
				Line:                githubv4mock.Ptr(githubv4.Int(42)),
				Side:                githubv4mock.Ptr(githubv4.DiffSideRight),
				PullRequestReviewID: githubv4mock.Ptr(githubv4.ID("PRR_123")),
			},
			nil,
			githubv4mock.DataResponse(map[string]any{
				"addPullRequestReviewThread": map[string]any{
					"thread": map[string]any{"id": "PRRT_456"},
				},
			}),
		),
	)
	gqlClient := githubv4.NewClient(mockedClient)
	deps := BaseDeps{GQLClient: gqlClient}
	serverTool := GranularAddPullRequestReviewComment(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"owner":       "owner",
		"repo":        "repo",
		"pullNumber":  float64(1),
		"path":        "src/main.go",
		"body":        "This needs a fix",
		"subjectType": "LINE",
		"line":        float64(42),
		"side":        "RIGHT",
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularResolveReviewThread(t *testing.T) {
	mockedClient := githubv4mock.NewMockedHTTPClient(
		githubv4mock.NewMutationMatcher(
			struct {
				ResolveReviewThread struct {
					Thread struct {
						ID         githubv4.ID
						IsResolved githubv4.Boolean
					}
				} `graphql:"resolveReviewThread(input: $input)"`
			}{},
			githubv4.ResolveReviewThreadInput{
				ThreadID: githubv4.ID("PRRT_123"),
			},
			nil,
			githubv4mock.DataResponse(map[string]any{
				"resolveReviewThread": map[string]any{
					"thread": map[string]any{"id": "PRRT_123", "isResolved": true},
				},
			}),
		),
	)
	gqlClient := githubv4.NewClient(mockedClient)
	deps := BaseDeps{GQLClient: gqlClient}
	serverTool := GranularResolveReviewThread(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"threadID": "PRRT_123",
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularUnresolveReviewThread(t *testing.T) {
	mockedClient := githubv4mock.NewMockedHTTPClient(
		githubv4mock.NewMutationMatcher(
			struct {
				UnresolveReviewThread struct {
					Thread struct {
						ID         githubv4.ID
						IsResolved githubv4.Boolean
					}
				} `graphql:"unresolveReviewThread(input: $input)"`
			}{},
			githubv4.UnresolveReviewThreadInput{
				ThreadID: githubv4.ID("PRRT_123"),
			},
			nil,
			githubv4mock.DataResponse(map[string]any{
				"unresolveReviewThread": map[string]any{
					"thread": map[string]any{"id": "PRRT_123", "isResolved": false},
				},
			}),
		),
	)
	gqlClient := githubv4.NewClient(mockedClient)
	deps := BaseDeps{GQLClient: gqlClient}
	serverTool := GranularUnresolveReviewThread(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"threadID": "PRRT_123",
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	assert.False(t, result.IsError)
}

func TestGranularSetIssueFields(t *testing.T) {
	t.Run("successful set with text value", func(t *testing.T) {
		matchers := []githubv4mock.Matcher{
			// Mock the issue ID query
			githubv4mock.NewQueryMatcher(
				struct {
					Repository struct {
						Issue struct {
							ID githubv4.ID
						} `graphql:"issue(number: $issueNumber)"`
					} `graphql:"repository(owner: $owner, name: $repo)"`
				}{},
				map[string]any{
					"owner":       githubv4.String("owner"),
					"repo":        githubv4.String("repo"),
					"issueNumber": githubv4.Int(5),
				},
				githubv4mock.DataResponse(map[string]any{
					"repository": map[string]any{
						"issue": map[string]any{"id": "ISSUE_123"},
					},
				}),
			),
			// Mock the setIssueFieldValue mutation
			githubv4mock.NewMutationMatcher(
				struct {
					SetIssueFieldValue struct {
						Issue struct {
							ID     githubv4.ID
							Number githubv4.Int
							URL    githubv4.String
						}
						IssueFieldValues []struct {
							TextValue struct {
								Value string
							} `graphql:"... on IssueFieldTextValue"`
							SingleSelectValue struct {
								Name string
							} `graphql:"... on IssueFieldSingleSelectValue"`
							DateValue struct {
								Value string
							} `graphql:"... on IssueFieldDateValue"`
							NumberValue struct {
								Value float64
							} `graphql:"... on IssueFieldNumberValue"`
						}
					} `graphql:"setIssueFieldValue(input: $input)"`
				}{},
				SetIssueFieldValueInput{
					IssueID: githubv4.ID("ISSUE_123"),
					IssueFields: []IssueFieldCreateOrUpdateInput{
						{
							FieldID:   githubv4.ID("FIELD_1"),
							TextValue: githubv4.NewString(githubv4.String("hello")),
						},
					},
				},
				nil,
				githubv4mock.DataResponse(map[string]any{
					"setIssueFieldValue": map[string]any{
						"issue": map[string]any{
							"id":     "ISSUE_123",
							"number": 5,
							"url":    "https://github.com/owner/repo/issues/5",
						},
					},
				}),
			),
		}

		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...))
		deps := BaseDeps{GQLClient: gqlClient}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields": []any{
				map[string]any{"field_id": "FIELD_1", "text_value": "hello"},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		assert.False(t, result.IsError)
	})

	t.Run("missing required parameter fields", func(t *testing.T) {
		deps := BaseDeps{}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "missing required parameter: fields")
	})

	t.Run("empty fields array", func(t *testing.T) {
		deps := BaseDeps{}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields":       []any{},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "fields array must not be empty")
	})

	t.Run("field missing value", func(t *testing.T) {
		deps := BaseDeps{}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields": []any{
				map[string]any{"field_id": "FIELD_1"},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "each field must have a value")
	})

	t.Run("multiple value keys returns error", func(t *testing.T) {
		deps := BaseDeps{}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields": []any{
				map[string]any{"field_id": "FIELD_1", "text_value": "hello", "number_value": float64(42)},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "each field must have exactly one value")
	})

	t.Run("value key with delete returns error", func(t *testing.T) {
		deps := BaseDeps{}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields": []any{
				map[string]any{"field_id": "FIELD_1", "text_value": "hello", "delete": true},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "each field must have exactly one value")
	})

	t.Run("successful set with text value and rationale", func(t *testing.T) {
		matchers := []githubv4mock.Matcher{
			githubv4mock.NewQueryMatcher(
				struct {
					Repository struct {
						Issue struct {
							ID githubv4.ID
						} `graphql:"issue(number: $issueNumber)"`
					} `graphql:"repository(owner: $owner, name: $repo)"`
				}{},
				map[string]any{
					"owner":       githubv4.String("owner"),
					"repo":        githubv4.String("repo"),
					"issueNumber": githubv4.Int(5),
				},
				githubv4mock.DataResponse(map[string]any{
					"repository": map[string]any{
						"issue": map[string]any{"id": "ISSUE_123"},
					},
				}),
			),
			githubv4mock.NewMutationMatcher(
				struct {
					SetIssueFieldValue struct {
						Issue struct {
							ID     githubv4.ID
							Number githubv4.Int
							URL    githubv4.String
						}
						IssueFieldValues []struct {
							TextValue struct {
								Value string
							} `graphql:"... on IssueFieldTextValue"`
							SingleSelectValue struct {
								Name string
							} `graphql:"... on IssueFieldSingleSelectValue"`
							DateValue struct {
								Value string
							} `graphql:"... on IssueFieldDateValue"`
							NumberValue struct {
								Value float64
							} `graphql:"... on IssueFieldNumberValue"`
						}
					} `graphql:"setIssueFieldValue(input: $input)"`
				}{},
				SetIssueFieldValueInput{
					IssueID: githubv4.ID("ISSUE_123"),
					IssueFields: []IssueFieldCreateOrUpdateInput{
						{
							FieldID:   githubv4.ID("FIELD_1"),
							TextValue: githubv4.NewString(githubv4.String("hello")),
							Rationale: githubv4.NewString(githubv4.String("Reflects the reported severity")),
						},
					},
				},
				nil,
				githubv4mock.DataResponse(map[string]any{
					"setIssueFieldValue": map[string]any{
						"issue": map[string]any{
							"id":     "ISSUE_123",
							"number": 5,
							"url":    "https://github.com/owner/repo/issues/5",
						},
					},
				}),
			),
		}

		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...))
		deps := BaseDeps{GQLClient: gqlClient}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields": []any{
				map[string]any{
					"field_id":   "FIELD_1",
					"text_value": "hello",
					"rationale":  "  Reflects the reported severity  ",
				},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		assert.False(t, result.IsError)
	})

	t.Run("rationale too long returns error", func(t *testing.T) {
		deps := BaseDeps{}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields": []any{
				map[string]any{
					"field_id":   "FIELD_1",
					"text_value": "hello",
					"rationale":  strings.Repeat("a", 281),
				},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "field rationale must be 280 characters or less")
	})

	t.Run("successful set with confidence", func(t *testing.T) {
		confidence := "HIGH"
		matchers := []githubv4mock.Matcher{
			githubv4mock.NewQueryMatcher(
				struct {
					Repository struct {
						Issue struct {
							ID githubv4.ID
						} `graphql:"issue(number: $issueNumber)"`
					} `graphql:"repository(owner: $owner, name: $repo)"`
				}{},
				map[string]any{
					"owner":       githubv4.String("owner"),
					"repo":        githubv4.String("repo"),
					"issueNumber": githubv4.Int(5),
				},
				githubv4mock.DataResponse(map[string]any{
					"repository": map[string]any{
						"issue": map[string]any{"id": "ISSUE_123"},
					},
				}),
			),
			githubv4mock.NewMutationMatcher(
				struct {
					SetIssueFieldValue struct {
						Issue struct {
							ID     githubv4.ID
							Number githubv4.Int
							URL    githubv4.String
						}
						IssueFieldValues []struct {
							TextValue struct {
								Value string
							} `graphql:"... on IssueFieldTextValue"`
							SingleSelectValue struct {
								Name string
							} `graphql:"... on IssueFieldSingleSelectValue"`
							DateValue struct {
								Value string
							} `graphql:"... on IssueFieldDateValue"`
							NumberValue struct {
								Value float64
							} `graphql:"... on IssueFieldNumberValue"`
						}
					} `graphql:"setIssueFieldValue(input: $input)"`
				}{},
				SetIssueFieldValueInput{
					IssueID: githubv4.ID("ISSUE_123"),
					IssueFields: []IssueFieldCreateOrUpdateInput{
						{
							FieldID:    githubv4.ID("FIELD_1"),
							TextValue:  githubv4.NewString(githubv4.String("hello")),
							Confidence: &confidence,
						},
					},
				},
				nil,
				githubv4mock.DataResponse(map[string]any{
					"setIssueFieldValue": map[string]any{
						"issue": map[string]any{
							"id":     "ISSUE_123",
							"number": 5,
							"url":    "https://github.com/owner/repo/issues/5",
						},
					},
				}),
			),
		}

		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...))
		deps := BaseDeps{GQLClient: gqlClient}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields": []any{
				map[string]any{
					"field_id":   "FIELD_1",
					"text_value": "hello",
					"confidence": " high ",
				},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		assert.False(t, result.IsError)
	})

	t.Run("invalid confidence value returns error", func(t *testing.T) {
		deps := BaseDeps{}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields": []any{
				map[string]any{
					"field_id":   "FIELD_1",
					"text_value": "hello",
					"confidence": "very_high",
				},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "confidence must be one of: LOW, MEDIUM, HIGH")
	})

	t.Run("confidence is sent when supplied", func(t *testing.T) {
		confidence := "HIGH"
		matchers := []githubv4mock.Matcher{
			githubv4mock.NewQueryMatcher(
				struct {
					Repository struct {
						Issue struct {
							ID githubv4.ID
						} `graphql:"issue(number: $issueNumber)"`
					} `graphql:"repository(owner: $owner, name: $repo)"`
				}{},
				map[string]any{
					"owner":       githubv4.String("owner"),
					"repo":        githubv4.String("repo"),
					"issueNumber": githubv4.Int(5),
				},
				githubv4mock.DataResponse(map[string]any{
					"repository": map[string]any{
						"issue": map[string]any{"id": "ISSUE_123"},
					},
				}),
			),
			githubv4mock.NewMutationMatcher(
				struct {
					SetIssueFieldValue struct {
						Issue struct {
							ID     githubv4.ID
							Number githubv4.Int
							URL    githubv4.String
						}
						IssueFieldValues []struct {
							TextValue struct {
								Value string
							} `graphql:"... on IssueFieldTextValue"`
							SingleSelectValue struct {
								Name string
							} `graphql:"... on IssueFieldSingleSelectValue"`
							DateValue struct {
								Value string
							} `graphql:"... on IssueFieldDateValue"`
							NumberValue struct {
								Value float64
							} `graphql:"... on IssueFieldNumberValue"`
						}
					} `graphql:"setIssueFieldValue(input: $input)"`
				}{},
				SetIssueFieldValueInput{
					IssueID: githubv4.ID("ISSUE_123"),
					IssueFields: []IssueFieldCreateOrUpdateInput{
						{
							FieldID:    githubv4.ID("FIELD_1"),
							TextValue:  githubv4.NewString(githubv4.String("hello")),
							Confidence: &confidence,
						},
					},
				},
				nil,
				githubv4mock.DataResponse(map[string]any{
					"setIssueFieldValue": map[string]any{
						"issue": map[string]any{
							"id":     "ISSUE_123",
							"number": 5,
							"url":    "https://github.com/owner/repo/issues/5",
						},
					},
				}),
			),
		}

		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...))
		deps := BaseDeps{GQLClient: gqlClient}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields": []any{
				map[string]any{
					"field_id":   "FIELD_1",
					"text_value": "hello",
					"confidence": "HIGH",
				},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		assert.False(t, result.IsError, getTextResult(t, result).Text)
	})

	t.Run("successful set with suggest flag", func(t *testing.T) {
		suggestTrue := githubv4.Boolean(true)
		matchers := []githubv4mock.Matcher{
			githubv4mock.NewQueryMatcher(
				struct {
					Repository struct {
						Issue struct {
							ID githubv4.ID
						} `graphql:"issue(number: $issueNumber)"`
					} `graphql:"repository(owner: $owner, name: $repo)"`
				}{},
				map[string]any{
					"owner":       githubv4.String("owner"),
					"repo":        githubv4.String("repo"),
					"issueNumber": githubv4.Int(5),
				},
				githubv4mock.DataResponse(map[string]any{
					"repository": map[string]any{
						"issue": map[string]any{"id": "ISSUE_123"},
					},
				}),
			),
			githubv4mock.NewMutationMatcher(
				struct {
					SetIssueFieldValue struct {
						Issue struct {
							ID     githubv4.ID
							Number githubv4.Int
							URL    githubv4.String
						}
						IssueFieldValues []struct {
							TextValue struct {
								Value string
							} `graphql:"... on IssueFieldTextValue"`
							SingleSelectValue struct {
								Name string
							} `graphql:"... on IssueFieldSingleSelectValue"`
							DateValue struct {
								Value string
							} `graphql:"... on IssueFieldDateValue"`
							NumberValue struct {
								Value float64
							} `graphql:"... on IssueFieldNumberValue"`
						}
					} `graphql:"setIssueFieldValue(input: $input)"`
				}{},
				SetIssueFieldValueInput{
					IssueID: githubv4.ID("ISSUE_123"),
					IssueFields: []IssueFieldCreateOrUpdateInput{
						{
							FieldID:   githubv4.ID("FIELD_1"),
							TextValue: githubv4.NewString(githubv4.String("hello")),
							Rationale: githubv4.NewString(githubv4.String("Reflects the reported severity")),
							Suggest:   &suggestTrue,
						},
					},
				},
				nil,
				githubv4mock.DataResponse(map[string]any{
					"setIssueFieldValue": map[string]any{
						"issue": map[string]any{
							"id":     "ISSUE_123",
							"number": 5,
							"url":    "https://github.com/owner/repo/issues/5",
						},
					},
				}),
			),
		}

		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matchers...))
		deps := BaseDeps{GQLClient: gqlClient}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields": []any{
				map[string]any{
					"field_id":      "FIELD_1",
					"text_value":    "hello",
					"rationale":     "Reflects the reported severity",
					"is_suggestion": true,
				},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		assert.False(t, result.IsError)
	})

	t.Run("sends GraphQL-Features: update_issue_suggestions header on mutation", func(t *testing.T) {
		matchers := []githubv4mock.Matcher{
			githubv4mock.NewQueryMatcher(
				struct {
					Repository struct {
						Issue struct {
							ID githubv4.ID
						} `graphql:"issue(number: $issueNumber)"`
					} `graphql:"repository(owner: $owner, name: $repo)"`
				}{},
				map[string]any{
					"owner":       githubv4.String("owner"),
					"repo":        githubv4.String("repo"),
					"issueNumber": githubv4.Int(5),
				},
				githubv4mock.DataResponse(map[string]any{
					"repository": map[string]any{
						"issue": map[string]any{"id": "ISSUE_123"},
					},
				}),
			),
			githubv4mock.NewMutationMatcher(
				struct {
					SetIssueFieldValue struct {
						Issue struct {
							ID     githubv4.ID
							Number githubv4.Int
							URL    githubv4.String
						}
						IssueFieldValues []struct {
							TextValue struct {
								Value string
							} `graphql:"... on IssueFieldTextValue"`
							SingleSelectValue struct {
								Name string
							} `graphql:"... on IssueFieldSingleSelectValue"`
							DateValue struct {
								Value string
							} `graphql:"... on IssueFieldDateValue"`
							NumberValue struct {
								Value float64
							} `graphql:"... on IssueFieldNumberValue"`
						}
					} `graphql:"setIssueFieldValue(input: $input)"`
				}{},
				SetIssueFieldValueInput{
					IssueID: githubv4.ID("ISSUE_123"),
					IssueFields: []IssueFieldCreateOrUpdateInput{
						{
							FieldID:   githubv4.ID("FIELD_1"),
							TextValue: githubv4.NewString(githubv4.String("hello")),
						},
					},
				},
				nil,
				githubv4mock.DataResponse(map[string]any{
					"setIssueFieldValue": map[string]any{
						"issue": map[string]any{
							"id":     "ISSUE_123",
							"number": 5,
							"url":    "https://github.com/owner/repo/issues/5",
						},
					},
				}),
			),
		}

		// Build a transport chain matching production: GraphQLFeaturesTransport
		// wraps a header-capturing spy, which forwards to the mock's RoundTripper.
		// This verifies the mutation request sets the update_issue_suggestions
		// feature flag so the rationale/suggest input fields are accepted.
		mockClient := githubv4mock.NewMockedHTTPClient(matchers...)
		spy := &headerCaptureTransport{inner: mockClient.Transport}
		httpClient := &http.Client{
			Transport: &transportpkg.GraphQLFeaturesTransport{Transport: spy},
		}
		gqlClient := githubv4.NewClient(httpClient)
		deps := BaseDeps{GQLClient: gqlClient}
		serverTool := GranularSetIssueFields(translations.NullTranslationHelper)
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(5),
			"fields": []any{
				map[string]any{"field_id": "FIELD_1", "text_value": "hello"},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		require.False(t, result.IsError, getTextResult(t, result).Text)
		// The last request captured is the mutation; the preceding issue ID
		// query does not require the feature flag.
		assert.Equal(t, "update_issue_suggestions", spy.captured.Get(headers.GraphQLFeaturesHeader))
	})
}

// --- Reaction granular tool handler tests ---

func TestGranularAddIssueReaction(t *testing.T) {
	mockReaction := &gogithub.Reaction{
		ID:      gogithub.Ptr(int64(12345)),
		Content: gogithub.Ptr("+1"),
	}

	tests := []struct {
		name         string
		mockedClient *http.Client
		args         map[string]any
		expectErr    bool
	}{
		{
			name: "add reaction to issue successfully",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesReactionsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusCreated, mockReaction),
			}),
			args: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"content":      "+1",
			},
			expectErr: false,
		},
		{
			name:         "missing owner returns error",
			mockedClient: MockHTTPClientWithHandlers(nil),
			args: map[string]any{
				"repo":         "repo",
				"issue_number": float64(42),
				"content":      "+1",
			},
			expectErr: true,
		},
		{
			name:         "missing content returns error",
			mockedClient: MockHTTPClientWithHandlers(nil),
			args: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectErr: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, tc.mockedClient)
			deps := BaseDeps{Client: client}
			serverTool := GranularAddIssueReaction(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)
			request := createMCPRequest(tc.args)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			if tc.expectErr {
				assert.True(t, result.IsError)
			} else {
				assert.False(t, result.IsError)
				textContent := getTextResult(t, result)
				var response MinimalResponse
				require.NoError(t, json.Unmarshal([]byte(textContent.Text), &response))
				assert.Equal(t, "12345", response.ID)
				assert.Equal(t, "https://api.github.com/repos/owner/repo/issues/42/reactions/12345", response.URL)
			}
		})
	}
}

func TestGranularAddIssueCommentReaction(t *testing.T) {
	mockReaction := &gogithub.Reaction{
		ID:      gogithub.Ptr(int64(67890)),
		Content: gogithub.Ptr("heart"),
	}

	tests := []struct {
		name         string
		mockedClient *http.Client
		args         map[string]any
		expectErr    bool
	}{
		{
			name: "add reaction to issue comment successfully",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesCommentsReactionsByOwnerByRepoByCommentID: mockResponse(t, http.StatusCreated, mockReaction),
			}),
			args: map[string]any{
				"owner":      "owner",
				"repo":       "repo",
				"comment_id": float64(999),
				"content":    "heart",
			},
			expectErr: false,
		},
		{
			name:         "missing comment_id returns error",
			mockedClient: MockHTTPClientWithHandlers(nil),
			args: map[string]any{
				"owner":   "owner",
				"repo":    "repo",
				"content": "heart",
			},
			expectErr: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, tc.mockedClient)
			deps := BaseDeps{Client: client}
			serverTool := GranularAddIssueCommentReaction(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)
			request := createMCPRequest(tc.args)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			if tc.expectErr {
				assert.True(t, result.IsError)
			} else {
				assert.False(t, result.IsError)
				textContent := getTextResult(t, result)
				var response MinimalResponse
				require.NoError(t, json.Unmarshal([]byte(textContent.Text), &response))
				assert.Equal(t, "67890", response.ID)
				assert.Equal(t, "https://api.github.com/repos/owner/repo/issues/comments/999/reactions/67890", response.URL)
			}
		})
	}
}

func TestGranularAddPullRequestReviewCommentReaction(t *testing.T) {
	mockReaction := &gogithub.Reaction{
		ID:      gogithub.Ptr(int64(54321)),
		Content: gogithub.Ptr("rocket"),
	}

	tests := []struct {
		name         string
		mockedClient *http.Client
		args         map[string]any
		expectErr    bool
	}{
		{
			name: "add reaction to PR review comment successfully",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposPullsCommentsReactionsByOwnerByRepoByCommentID: mockResponse(t, http.StatusCreated, mockReaction),
			}),
			args: map[string]any{
				"owner":      "owner",
				"repo":       "repo",
				"comment_id": float64(888),
				"content":    "rocket",
			},
			expectErr: false,
		},
		{
			name:         "missing repo returns error",
			mockedClient: MockHTTPClientWithHandlers(nil),
			args: map[string]any{
				"owner":      "owner",
				"comment_id": float64(888),
				"content":    "rocket",
			},
			expectErr: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, tc.mockedClient)
			deps := BaseDeps{Client: client}
			serverTool := GranularAddPullRequestReviewCommentReaction(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)
			request := createMCPRequest(tc.args)
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			if tc.expectErr {
				assert.True(t, result.IsError)
			} else {
				assert.False(t, result.IsError)
				textContent := getTextResult(t, result)
				var response MinimalResponse
				require.NoError(t, json.Unmarshal([]byte(textContent.Text), &response))
				assert.Equal(t, "54321", response.ID)
				assert.Equal(t, "https://api.github.com/repos/owner/repo/pulls/comments/888/reactions/54321", response.URL)
			}
		})
	}
}
