package github

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"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"
	"github.com/google/go-github/v89/github"
	"github.com/google/jsonschema-go/jsonschema"
	"github.com/shurcooL/githubv4"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var defaultGQLClient *githubv4.Client = githubv4.NewClient(newRepoAccessHTTPClient())

type repoAccessKey struct {
	owner string
	repo  string
}

type repoAccessValue struct {
	isPrivate bool
}

type repoAccessMockTransport struct {
	responses map[repoAccessKey]repoAccessValue
}

func newRepoAccessHTTPClient() *http.Client {
	responses := map[repoAccessKey]repoAccessValue{
		{owner: "owner2", repo: "repo2"}: {isPrivate: true},
		{owner: "owner", repo: "repo"}:   {isPrivate: false},
	}

	return &http.Client{Transport: &repoAccessMockTransport{responses: responses}}
}

const issueReadEnrichmentQueryString = "query($ids:[ID!]!){nodes(ids: $ids){... on Issue{id,issueFieldValues(first: 25){nodes{__typename,... on IssueFieldDateValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldNumberValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},valueNumber: value},... on IssueFieldSingleSelectValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldTextValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value}}},parent{number,title,state,url,author{login},repository{nameWithOwner}},subIssuesSummary{total,completed,percentCompleted}}}}"

// newIssueReadEnrichmentMatcher builds a matcher for the issue_read `get` enrichment query for a
// single issue node ID.
func newIssueReadEnrichmentMatcher(nodeID string, response githubv4mock.GQLResponse) githubv4mock.Matcher {
	return githubv4mock.NewQueryMatcher(
		issueReadEnrichmentQueryString,
		map[string]any{"ids": []any{nodeID}},
		response,
	)
}

func (rt *repoAccessMockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	if req.Body == nil {
		return nil, fmt.Errorf("missing request body")
	}

	var payload struct {
		Query     string         `json:"query"`
		Variables map[string]any `json:"variables"`
	}

	if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
		return nil, err
	}
	_ = req.Body.Close()

	owner := toString(payload.Variables["owner"])
	repo := toString(payload.Variables["name"])

	value, ok := rt.responses[repoAccessKey{owner: owner, repo: repo}]
	if !ok {
		value = repoAccessValue{isPrivate: false}
	}

	data := map[string]any{}
	if strings.Contains(payload.Query, "viewer") {
		data["viewer"] = map[string]any{"login": "test-viewer"}
	}
	if strings.Contains(payload.Query, "repository") {
		data["repository"] = map[string]any{"isPrivate": value.isPrivate}
	}

	responseBody, err := json.Marshal(map[string]any{"data": data})
	if err != nil {
		return nil, err
	}

	resp := &http.Response{
		StatusCode: http.StatusOK,
		Header:     make(http.Header),
		Body:       io.NopCloser(bytes.NewReader(responseBody)),
	}
	resp.Header.Set("Content-Type", "application/json")
	return resp, nil
}

func toString(v any) string {
	switch value := v.(type) {
	case string:
		return value
	case fmt.Stringer:
		return value.String()
	case nil:
		return ""
	default:
		return fmt.Sprintf("%v", value)
	}
}

func Test_GetIssue(t *testing.T) {
	// Verify tool definition once
	serverTool := IssueRead(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "issue_read", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "method")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_number")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo", "issue_number"})

	// Setup mock issue for success case
	mockIssue := &github.Issue{
		Number:  github.Ptr(42),
		Title:   github.Ptr("Test Issue"),
		Body:    github.Ptr("This is a test issue"),
		State:   github.Ptr("open"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"),
		User: &github.User{
			Login: github.Ptr("testuser"),
		},
		Repository: &github.Repository{
			Name: github.Ptr("repo"),
			Owner: &github.User{
				Login: github.Ptr("owner"),
			},
		},
	}
	mockIssue2 := &github.Issue{
		Number:  github.Ptr(422),
		Title:   github.Ptr("Test Issue 2"),
		Body:    github.Ptr("This is a test issue 2"),
		State:   github.Ptr("open"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"),
		User: &github.User{
			Login: github.Ptr("testuser2"),
		},
		Repository: &github.Repository{
			Name: github.Ptr("repo2"),
			Owner: &github.User{
				Login: github.Ptr("owner2"),
			},
		},
	}

	tests := []struct {
		name               string
		mockedClient       *http.Client
		requestArgs        map[string]any
		expectHandlerError bool
		expectResultError  bool
		expectedIssue      *github.Issue
		expectedErrMsg     string
		lockdownEnabled    bool
		restPermission     string
	}{
		{
			name: "successful issue retrieval",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue),
			}),
			requestArgs: map[string]any{
				"method":       "get",
				"owner":        "owner2",
				"repo":         "repo2",
				"issue_number": float64(42),
			},
			expectedIssue: mockIssue,
		},
		{
			name: "issue not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message": "Issue not found"}`),
			}),
			requestArgs: map[string]any{
				"method":       "get",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(999),
			},
			expectHandlerError: true,
			expectedErrMsg:     "failed to get issue",
		},
		{
			name: "lockdown enabled - private repository",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue2),
			}),
			requestArgs: map[string]any{
				"method":       "get",
				"owner":        "owner2",
				"repo":         "repo2",
				"issue_number": float64(422),
			},
			expectedIssue:   mockIssue2,
			lockdownEnabled: true,
			restPermission:  "none",
		},
		{
			name: "lockdown enabled - user lacks push access",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue),
			}),
			requestArgs: map[string]any{
				"method":       "get",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectResultError: true,
			expectedErrMsg:    "access to issue details is restricted by lockdown mode",
			lockdownEnabled:   true,
			restPermission:    "read",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := mustNewGHClient(t, tc.mockedClient)

			var restClient *github.Client
			if tc.restPermission != "" {
				restClient = mockRESTPermissionServer(t, tc.restPermission, nil)
			}
			cache := stubRepoAccessCache(restClient, 15*time.Minute)

			flags := stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled})
			deps := BaseDeps{
				Client:          client,
				GQLClient:       defaultGQLClient,
				RepoAccessCache: cache,
				Flags:           flags,
			}
			handler := serverTool.Handler(deps)

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

			if tc.expectHandlerError {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.expectedErrMsg)
				return
			}

			require.NoError(t, err)
			require.NotNil(t, result)

			if tc.expectResultError {
				errorContent := getErrorResult(t, result)
				assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
				return
			}

			textContent := getTextResult(t, result)

			var returnedIssue MinimalIssue
			err = json.Unmarshal([]byte(textContent.Text), &returnedIssue)
			require.NoError(t, err)
			assert.Equal(t, tc.expectedIssue.GetNumber(), returnedIssue.Number)
			assert.Equal(t, tc.expectedIssue.GetTitle(), returnedIssue.Title)
			assert.Equal(t, tc.expectedIssue.GetBody(), returnedIssue.Body)
			assert.Equal(t, tc.expectedIssue.GetState(), returnedIssue.State)
			assert.Equal(t, tc.expectedIssue.GetHTMLURL(), returnedIssue.HTMLURL)
			assert.Equal(t, tc.expectedIssue.GetUser().GetLogin(), returnedIssue.User.Login)
		})
	}
}

func Test_IssueRead_IFC_InsidersMode(t *testing.T) {
	t.Parallel()

	serverTool := IssueRead(translations.NullTranslationHelper)

	mockIssue := &github.Issue{
		Number:  github.Ptr(1),
		Title:   github.Ptr("Test"),
		Body:    github.Ptr("body"),
		State:   github.Ptr("open"),
		HTMLURL: github.Ptr("https://github.com/octocat/repo/issues/1"),
		User:    &github.User{Login: github.Ptr("u")},
	}

	mockComments := []*github.IssueComment{
		{Body: github.Ptr("hello"), User: &github.User{Login: github.Ptr("u")}},
	}

	makeMockClient := func(isPrivate bool, repoStatus int) *http.Client {
		handlers := map[string]http.HandlerFunc{
			GetReposIssuesByOwnerByRepoByIssueNumber:         mockResponse(t, http.StatusOK, mockIssue),
			GetReposIssuesCommentsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockComments),
		}
		if repoStatus != 0 && repoStatus != http.StatusOK {
			handlers[GetReposByOwnerByRepo] = mockResponse(t, repoStatus, "boom")
		} else {
			handlers[GetReposByOwnerByRepo] = mockResponse(t, http.StatusOK, map[string]any{
				"name":    "repo",
				"private": isPrivate,
			})
		}
		return MockHTTPClientWithHandlers(handlers)
	}

	getReq := map[string]any{
		"method":       "get",
		"owner":        "octocat",
		"repo":         "repo",
		"issue_number": float64(1),
	}
	commentsReq := map[string]any{
		"method":       "get_comments",
		"owner":        "octocat",
		"repo":         "repo",
		"issue_number": float64(1),
	}

	t.Run("insiders mode disabled omits ifc label", func(t *testing.T) {
		deps := BaseDeps{
			Client: mustNewGHClient(t, makeMockClient(false, 0)),
		}
		handler := serverTool.Handler(deps)

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

		assert.Nil(t, result.Meta)
	})

	t.Run("insiders mode enabled on public repo emits public untrusted", func(t *testing.T) {
		deps := BaseDeps{
			Client:         mustNewGHClient(t, makeMockClient(false, 0)),
			featureChecker: featureCheckerFor(FeatureFlagIFCLabels),
		}
		handler := serverTool.Handler(deps)

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

		require.NotNil(t, result.Meta)
		ifcMap := unmarshalIFC(t, result.Meta["ifc"])
		assert.Equal(t, "untrusted", ifcMap["integrity"])
		assert.Equal(t, "public", ifcMap["confidentiality"])
	})

	t.Run("insiders mode enabled on private repo with get_comments emits private trusted", func(t *testing.T) {
		deps := BaseDeps{
			Client:         mustNewGHClient(t, makeMockClient(true, 0)),
			featureChecker: featureCheckerFor(FeatureFlagIFCLabels),
		}
		handler := serverTool.Handler(deps)

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

		require.NotNil(t, result.Meta)
		ifcMap := unmarshalIFC(t, result.Meta["ifc"])
		assert.Equal(t, "trusted", ifcMap["integrity"])
		assert.Equal(t, "private", ifcMap["confidentiality"])
	})

	t.Run("insiders mode skips ifc label when visibility lookup fails", func(t *testing.T) {
		deps := BaseDeps{
			Client:         mustNewGHClient(t, makeMockClient(false, http.StatusInternalServerError)),
			featureChecker: featureCheckerFor(FeatureFlagIFCLabels),
		}
		handler := serverTool.Handler(deps)

		request := createMCPRequest(getReq)
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		require.False(t, result.IsError, "tool call should still succeed when visibility lookup fails")

		if result.Meta != nil {
			_, hasIFC := result.Meta["ifc"]
			assert.False(t, hasIFC, "ifc label should be omitted when visibility lookup fails")
		}
	})
}

func Test_GetIssue_FieldValues(t *testing.T) {
	// The raw REST issue_field_values are always cleared. Enriched field_values are
	// only populated via GraphQL when the issue has a node ID; this issue has none,
	// so field_values stays empty.
	serverTool := IssueRead(translations.NullTranslationHelper)

	mockIssueWithFields := &github.Issue{
		Number:  github.Ptr(99),
		Title:   github.Ptr("Issue with field values"),
		Body:    github.Ptr("body"),
		State:   github.Ptr("open"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/99"),
		User: &github.User{
			Login: github.Ptr("testuser"),
		},
		IssueFieldValues: []*github.IssueFieldValue{
			{
				IssueFieldID: 1001,
				NodeID:       "FV_node_1",
				DataType:     "single_select",
				Value:        "High",
				SingleSelectOption: &github.IssueFieldValueSingleSelectOption{
					ID:    42,
					Name:  "High",
					Color: "red",
				},
			},
			{
				IssueFieldID: 1002,
				NodeID:       "FV_node_2",
				DataType:     "text",
				Value:        "some text value",
			},
		},
	}

	mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssueWithFields),
	})

	cache := stubRepoAccessCache(nil, 15*time.Minute)
	flags := stubFeatureFlags(map[string]bool{"lockdown-mode": false})
	deps := BaseDeps{
		Client:          mustNewGHClient(t, mockedClient),
		GQLClient:       defaultGQLClient,
		RepoAccessCache: cache,
		Flags:           flags,
	}
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"method":       "get",
		"owner":        "owner",
		"repo":         "repo",
		"issue_number": float64(99),
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	require.NotNil(t, result)

	textContent := getTextResult(t, result)

	var returnedIssue MinimalIssue
	err = json.Unmarshal([]byte(textContent.Text), &returnedIssue)
	require.NoError(t, err)

	// Raw REST IssueFieldValues must be cleared, and no enriched field_values are
	// present because this issue has no node ID.
	assert.Empty(t, returnedIssue.IssueFieldValues, "raw REST issue_field_values should not be exposed")
	assert.Empty(t, returnedIssue.FieldValues, "enriched field_values should not be present without a node ID")
}

func Test_GetIssue_FieldValues_Enriched(t *testing.T) {
	// Verify the enriched field_values are populated via GraphQL when the issue has
	// a node ID, and the raw REST issue_field_values stays cleared.
	serverTool := IssueRead(translations.NullTranslationHelper)

	mockIssueWithFields := &github.Issue{
		Number:  github.Ptr(99),
		NodeID:  github.Ptr("I_node_99"),
		Title:   github.Ptr("Issue with field values"),
		Body:    github.Ptr("body"),
		State:   github.Ptr("open"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/99"),
		User: &github.User{
			Login: github.Ptr("testuser"),
		},
		IssueFieldValues: []*github.IssueFieldValue{
			{
				IssueFieldID: 1001,
				NodeID:       "FV_node_1",
				DataType:     "single_select",
				Value:        "High",
			},
		},
	}

	restClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssueWithFields),
	})

	gqlResponse := githubv4mock.DataResponse(map[string]any{
		"nodes": []map[string]any{
			{
				"id": "I_node_99",
				"issueFieldValues": map[string]any{
					"nodes": []map[string]any{
						{
							"__typename": "IssueFieldSingleSelectValue",
							"field":      map[string]any{"name": "priority"},
							"value":      "P1",
						},
						{
							"__typename":  "IssueFieldNumberValue",
							"field":       map[string]any{"name": "estimate"},
							"valueNumber": 2.5,
						},
					},
				},
				"parent":           nil,
				"subIssuesSummary": map[string]any{"total": 0, "completed": 0, "percentCompleted": 0},
			},
		},
	})

	matcher := newIssueReadEnrichmentMatcher("I_node_99", gqlResponse)
	gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))

	cache := stubRepoAccessCache(nil, 15*time.Minute)
	deps := BaseDeps{
		Client:          mustNewGHClient(t, restClient),
		GQLClient:       gqlClient,
		RepoAccessCache: cache,
	}
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"method":       "get",
		"owner":        "owner",
		"repo":         "repo",
		"issue_number": float64(99),
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	require.NotNil(t, result)
	require.False(t, result.IsError, "expected result to not be an error")

	textContent := getTextResult(t, result)

	var returnedIssue MinimalIssue
	err = json.Unmarshal([]byte(textContent.Text), &returnedIssue)
	require.NoError(t, err)

	// Raw REST IssueFieldValues is always cleared.
	assert.Empty(t, returnedIssue.IssueFieldValues, "raw REST issue_field_values should not be exposed")

	// Enriched FieldValues comes from the GraphQL nodes() round-trip.
	require.Len(t, returnedIssue.FieldValues, 2, "field_values should be populated from GraphQL")
	assert.Equal(t, "priority", returnedIssue.FieldValues[0].Field)
	assert.Equal(t, "P1", returnedIssue.FieldValues[0].Value)
	assert.Equal(t, "estimate", returnedIssue.FieldValues[1].Field)
	assert.Equal(t, "2.5", returnedIssue.FieldValues[1].Value)

	// With no parent and no sub-issues, the routing booleans are explicit false and the
	// optional relationship payloads are omitted.
	assert.Equal(t, github.Ptr(false), returnedIssue.HasParent, "has_parent should be false without a parent")
	assert.Equal(t, github.Ptr(false), returnedIssue.HasChildren, "has_children should be false without sub-issues")
	assert.Nil(t, returnedIssue.Parent, "parent should be omitted when there is no parent")
	assert.Nil(t, returnedIssue.SubIssuesSummary, "sub_issues_summary should be omitted with no sub-issues")
}

func Test_GetIssue_HierarchyEnrichment(t *testing.T) {
	mockIssue := &github.Issue{
		Number:  github.Ptr(2990),
		NodeID:  github.Ptr("I_node_2990"),
		Title:   github.Ptr("Child issue"),
		State:   github.Ptr("open"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/2990"),
		User:    &github.User{Login: github.Ptr("author")},
	}

	parentNode := map[string]any{
		"number": 2820,
		"title":  "Parent issue",
		"state":  "OPEN",
		"url":    "https://github.com/owner/repo/issues/2820",
		"author": map[string]any{"login": "parentauthor"},
		"repository": map[string]any{
			"nameWithOwner": "owner/repo",
		},
	}

	tests := []struct {
		name           string
		parent         any
		summary        map[string]any
		lockdown       bool
		assertResponse func(t *testing.T, issue MinimalIssue)
	}{
		{
			name:    "parent and children present",
			parent:  parentNode,
			summary: map[string]any{"total": 4, "completed": 1, "percentCompleted": 25},
			assertResponse: func(t *testing.T, issue MinimalIssue) {
				assert.Equal(t, github.Ptr(true), issue.HasParent)
				assert.Equal(t, github.Ptr(true), issue.HasChildren)
				require.NotNil(t, issue.Parent)
				assert.Equal(t, 2820, issue.Parent.Number)
				assert.Equal(t, "Parent issue", issue.Parent.Title)
				assert.Equal(t, "OPEN", issue.Parent.State)
				assert.Equal(t, "owner/repo", issue.Parent.Repository)
				require.NotNil(t, issue.SubIssuesSummary)
				assert.Equal(t, 4, issue.SubIssuesSummary.Total)
				assert.Equal(t, 1, issue.SubIssuesSummary.Completed)
				assert.Equal(t, 25, issue.SubIssuesSummary.PercentCompleted)
			},
		},
		{
			name:    "no parent omits parent and sets has_parent false",
			parent:  nil,
			summary: map[string]any{"total": 0, "completed": 0, "percentCompleted": 0},
			assertResponse: func(t *testing.T, issue MinimalIssue) {
				assert.Equal(t, github.Ptr(false), issue.HasParent)
				assert.Nil(t, issue.Parent)
			},
		},
		{
			name:    "has_children is false when total is zero even with completed nonzero",
			parent:  nil,
			summary: map[string]any{"total": 0, "completed": 1, "percentCompleted": 0},
			assertResponse: func(t *testing.T, issue MinimalIssue) {
				assert.Equal(t, github.Ptr(false), issue.HasChildren)
				assert.Nil(t, issue.SubIssuesSummary)
			},
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			restClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue),
			})

			gqlResponse := githubv4mock.DataResponse(map[string]any{
				"nodes": []map[string]any{
					{
						"id":               "I_node_2990",
						"issueFieldValues": map[string]any{"nodes": []map[string]any{}},
						"parent":           tc.parent,
						"subIssuesSummary": tc.summary,
					},
				},
			})
			matcher := newIssueReadEnrichmentMatcher("I_node_2990", gqlResponse)
			gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))

			deps := BaseDeps{
				Client:          mustNewGHClient(t, restClient),
				GQLClient:       gqlClient,
				RepoAccessCache: stubRepoAccessCache(nil, 15*time.Minute),
				Flags:           stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdown}),
			}
			serverTool := IssueRead(translations.NullTranslationHelper)
			handler := serverTool.Handler(deps)

			request := createMCPRequest(map[string]any{
				"method":       "get",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(2990),
			})
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)
			require.NoError(t, err)
			require.NotNil(t, result)
			require.False(t, result.IsError, "expected result to not be an error")

			var returnedIssue MinimalIssue
			require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &returnedIssue))
			tc.assertResponse(t, returnedIssue)
		})
	}
}

func Test_GetIssue_HierarchyEnrichment_Lockdown(t *testing.T) {
	mockIssue := &github.Issue{
		Number:  github.Ptr(2990),
		NodeID:  github.Ptr("I_node_2990"),
		Title:   github.Ptr("Child issue"),
		State:   github.Ptr("open"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/2990"),
		User:    &github.User{Login: github.Ptr("author")},
	}

	parentNode := map[string]any{
		"number": 2820,
		"title":  "Sensitive parent title",
		"state":  "OPEN",
		"url":    "https://github.com/owner/repo/issues/2820",
		"author": map[string]any{"login": "parentauthor"},
		"repository": map[string]any{
			"nameWithOwner": "owner/repo",
		},
	}

	// In lockdown mode the issue's own author must be verified as safe (mirrors the existing
	// REST lockdown gate). The repo-access cache performs push-access checks against its own
	// REST client: the issue author ("author") has write access, while the parent author
	// ("parentauthor") only has read access and so cannot be verified as safe. The parent
	// reference is therefore omitted entirely, while has_parent stays true so an agent can
	// still route to get_parent.
	restClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue),
	})
	permClient := mockRESTPermissionServer(t, "read", map[string]string{"author": "write"})

	gqlResponse := githubv4mock.DataResponse(map[string]any{
		"nodes": []map[string]any{
			{
				"id":               "I_node_2990",
				"issueFieldValues": map[string]any{"nodes": []map[string]any{}},
				"parent":           parentNode,
				"subIssuesSummary": map[string]any{"total": 0, "completed": 0, "percentCompleted": 0},
			},
		},
	})
	matcher := newIssueReadEnrichmentMatcher("I_node_2990", gqlResponse)
	gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))

	deps := BaseDeps{
		Client:          mustNewGHClient(t, restClient),
		GQLClient:       gqlClient,
		RepoAccessCache: stubRepoAccessCache(permClient, 15*time.Minute),
		Flags:           stubFeatureFlags(map[string]bool{"lockdown-mode": true}),
	}
	serverTool := IssueRead(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"method":       "get",
		"owner":        "owner",
		"repo":         "repo",
		"issue_number": float64(2990),
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	require.NotNil(t, result)
	require.False(t, result.IsError, "expected result to not be an error")

	var returnedIssue MinimalIssue
	require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &returnedIssue))

	require.Nil(t, returnedIssue.Parent, "parent reference should be omitted under lockdown when it cannot be verified safe")
	assert.Equal(t, github.Ptr(true), returnedIssue.HasParent, "has_parent should still be true so agents can route to get_parent")
}

func Test_GetIssue_HierarchyEnrichment_QueryFailureReturnsBaseIssue(t *testing.T) {
	mockIssue := &github.Issue{
		Number:  github.Ptr(2990),
		NodeID:  github.Ptr("I_node_2990"),
		Title:   github.Ptr("Child issue"),
		State:   github.Ptr("open"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/2990"),
		User:    &github.User{Login: github.Ptr("author")},
	}

	restClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		GetReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue),
	})

	matcher := newIssueReadEnrichmentMatcher("I_node_2990", githubv4mock.ErrorResponse("enrichment failed"))
	gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))

	deps := BaseDeps{
		Client:          mustNewGHClient(t, restClient),
		GQLClient:       gqlClient,
		RepoAccessCache: stubRepoAccessCache(nil, 15*time.Minute),
		Flags:           stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
	}
	serverTool := IssueRead(translations.NullTranslationHelper)
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"method":       "get",
		"owner":        "owner",
		"repo":         "repo",
		"issue_number": float64(2990),
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	require.NotNil(t, result)
	// Relationship enrichment must never fail `get`: the base issue is still returned.
	require.False(t, result.IsError, "enrichment failure should not fail get")

	var returnedIssue MinimalIssue
	require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &returnedIssue))
	assert.Equal(t, 2990, returnedIssue.Number)
	assert.Nil(t, returnedIssue.HasParent)
	assert.Nil(t, returnedIssue.HasChildren)
	assert.Nil(t, returnedIssue.Parent)
	assert.Nil(t, returnedIssue.SubIssuesSummary)
}

func Test_SearchIssues(t *testing.T) {
	// Verify tool definition once
	serverTool := SearchIssues(translations.NullTranslationHelper)
	tool := serverTool.Tool
	// SearchIssues is the FeatureFlagFieldsParam-enabled variant; it owns the
	// _ff_<flag> snapshot. The canonical search_issues.snap is owned by
	// LegacySearchIssues (see Test_LegacySearchIssues_Definition).
	require.NoError(t, toolsnaps.Test(tool.Name+"_ff_"+FeatureFlagFieldsParam, tool))
	require.Equal(t, FeatureFlagFieldsParam, serverTool.FeatureFlagEnable)

	assert.Equal(t, "search_issues", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "query")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "sort")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "order")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "perPage")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "page")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "fields")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"query"})

	// Setup mock search results
	mockSearchResult := &github.IssuesSearchResult{
		Total:             github.Ptr(2),
		IncompleteResults: github.Ptr(false),
		Issues: []*github.Issue{
			{
				Number:   github.Ptr(42),
				Title:    github.Ptr("Bug: Something is broken"),
				Body:     github.Ptr("This is a bug report"),
				State:    github.Ptr("open"),
				HTMLURL:  github.Ptr("https://github.com/owner/repo/issues/42"),
				Comments: github.Ptr(5),
				User: &github.User{
					Login: github.Ptr("user1"),
				},
			},
			{
				Number:   github.Ptr(43),
				Title:    github.Ptr("Feature: Add new functionality"),
				Body:     github.Ptr("This is a feature request"),
				State:    github.Ptr("open"),
				HTMLURL:  github.Ptr("https://github.com/owner/repo/issues/43"),
				Comments: github.Ptr(3),
				User: &github.User{
					Login: github.Ptr("user2"),
				},
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedResult *github.IssuesSearchResult
		expectedErrMsg string
	}{
		{
			name: "successful issues search with all parameters",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: expectQueryParams(
					t,
					map[string]string{
						"q":        "is:issue repo:owner/repo is:open",
						"sort":     "created",
						"order":    "desc",
						"page":     "1",
						"per_page": "30",
					},
				).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query":   "repo:owner/repo is:open",
				"sort":    "created",
				"order":   "desc",
				"page":    float64(1),
				"perPage": float64(30),
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "issues search with owner and repo parameters",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: expectQueryParams(
					t,
					map[string]string{
						"q":        "repo:test-owner/test-repo is:issue is:open",
						"sort":     "created",
						"order":    "asc",
						"page":     "1",
						"per_page": "30",
					},
				).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "is:open",
				"owner": "test-owner",
				"repo":  "test-repo",
				"sort":  "created",
				"order": "asc",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "issues search with only owner parameter (should ignore it)",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: expectQueryParams(
					t,
					map[string]string{
						"q":        "is:issue bug",
						"page":     "1",
						"per_page": "30",
					},
				).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "bug",
				"owner": "test-owner",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "issues search with only repo parameter (should ignore it)",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: expectQueryParams(
					t,
					map[string]string{
						"q":        "is:issue feature",
						"page":     "1",
						"per_page": "30",
					},
				).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "feature",
				"repo":  "test-repo",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "issues search with minimal parameters",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: mockResponse(t, http.StatusOK, mockSearchResult),
			}),
			requestArgs: map[string]any{
				"query": "is:issue repo:owner/repo is:open",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "query with existing is:issue filter - no duplication",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: expectQueryParams(
					t,
					map[string]string{
						"q":        "repo:github/github-mcp-server is:issue is:open (label:critical OR label:urgent)",
						"page":     "1",
						"per_page": "30",
					},
				).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "repo:github/github-mcp-server is:issue is:open (label:critical OR label:urgent)",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "query with existing repo: filter and conflicting owner/repo params - uses query filter",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: expectQueryParams(
					t,
					map[string]string{
						"q":        "is:issue repo:github/github-mcp-server critical",
						"page":     "1",
						"per_page": "30",
					},
				).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "repo:github/github-mcp-server critical",
				"owner": "different-owner",
				"repo":  "different-repo",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "query with both is: and repo: filters already present",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: expectQueryParams(
					t,
					map[string]string{
						"q":        "is:issue repo:octocat/Hello-World bug",
						"page":     "1",
						"per_page": "30",
					},
				).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "is:issue repo:octocat/Hello-World bug",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "complex query with multiple OR operators and existing filters",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: expectQueryParams(
					t,
					map[string]string{
						"q":        "repo:github/github-mcp-server is:issue (label:critical OR label:urgent OR label:high-priority OR label:blocker)",
						"page":     "1",
						"per_page": "30",
					},
				).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "repo:github/github-mcp-server is:issue (label:critical OR label:urgent OR label:high-priority OR label:blocker)",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "query with field. qualifier enables advanced_search",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: expectQueryParams(
					t,
					map[string]string{
						"q":               "is:issue field.priority:P1",
						"page":            "1",
						"per_page":        "30",
						"advanced_search": "true",
					},
				).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "field.priority:P1",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "query without field. qualifier does not set advanced_search",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: expectQueryParams(
					t,
					map[string]string{
						"q":        "is:issue is:open",
						"page":     "1",
						"per_page": "30",
					},
				).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "is:open",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "search issues fails",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchIssues: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusBadRequest)
					_, _ = w.Write([]byte(`{"message": "Validation Failed"}`))
				}),
			}),
			requestArgs: map[string]any{
				"query": "invalid:query",
			},
			expectError:    true,
			expectedErrMsg: "failed to search issues",
		},
	}

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

			// Create call request
			request := createMCPRequest(tc.requestArgs)

			// Call handler
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)

			// Verify results
			if tc.expectError {
				require.NoError(t, err) // No Go error, but result should be an error
				require.NotNil(t, result)
				require.True(t, result.IsError, "expected result to be an error")
				textContent := getErrorResult(t, result)
				assert.Contains(t, textContent.Text, tc.expectedErrMsg)
				return
			}

			require.NoError(t, err)
			require.False(t, result.IsError, "expected result to not be an error")

			// Parse the result and get the text content if no error
			textContent := getTextResult(t, result)

			// Unmarshal and verify the result
			var returnedResult github.IssuesSearchResult
			err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
			require.NoError(t, err)
			assert.Equal(t, *tc.expectedResult.Total, *returnedResult.Total)
			assert.Equal(t, *tc.expectedResult.IncompleteResults, *returnedResult.IncompleteResults)
			assert.Len(t, returnedResult.Issues, len(tc.expectedResult.Issues))
			for i, issue := range returnedResult.Issues {
				assert.Equal(t, *tc.expectedResult.Issues[i].Number, *issue.Number)
				assert.Equal(t, *tc.expectedResult.Issues[i].Title, *issue.Title)
				assert.Equal(t, *tc.expectedResult.Issues[i].State, *issue.State)
				assert.Equal(t, *tc.expectedResult.Issues[i].HTMLURL, *issue.HTMLURL)
				assert.Equal(t, *tc.expectedResult.Issues[i].User.Login, *issue.User.Login)
			}
		})
	}
}

func Test_SearchIssues_IFC_InsidersMode(t *testing.T) {
	t.Parallel()

	serverTool := SearchIssues(translations.NullTranslationHelper)

	makeIssue := func(owner, repo string, number int) *github.Issue {
		return &github.Issue{
			Number:        github.Ptr(number),
			Title:         github.Ptr("issue"),
			State:         github.Ptr("open"),
			RepositoryURL: github.Ptr("https://api.github.com/repos/" + owner + "/" + repo),
			User:          &github.User{Login: github.Ptr("u")},
		}
	}

	type repoFixture struct {
		owner      string
		repo       string
		isPrivate  bool
		repoStatus int
	}

	repoHandlers := func(repos []repoFixture) map[string]http.HandlerFunc {
		repoByPath := map[string]repoFixture{}
		for _, r := range repos {
			repoByPath["/repos/"+r.owner+"/"+r.repo] = r
		}
		return map[string]http.HandlerFunc{
			GetReposByOwnerByRepo: func(w http.ResponseWriter, req *http.Request) {
				r, ok := repoByPath[req.URL.Path]
				if !ok {
					w.WriteHeader(http.StatusNotFound)
					return
				}
				if r.repoStatus != 0 && r.repoStatus != http.StatusOK {
					w.WriteHeader(r.repoStatus)
					return
				}
				body, _ := json.Marshal(map[string]any{
					"name":    r.repo,
					"private": r.isPrivate,
				})
				w.WriteHeader(http.StatusOK)
				_, _ = w.Write(body)
			},
		}
	}

	makeMockClient := func(searchResult *github.IssuesSearchResult, repos []repoFixture) *http.Client {
		handlers := repoHandlers(repos)
		handlers[GetSearchIssues] = mockResponse(t, http.StatusOK, searchResult)
		return MockHTTPClientWithHandlers(handlers)
	}

	reqParams := map[string]any{"query": "bug"}

	t.Run("insiders mode disabled omits ifc label", func(t *testing.T) {
		searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{makeIssue("octocat", "public-repo", 1)}}
		deps := BaseDeps{
			Client: mustNewGHClient(t, makeMockClient(searchResult, []repoFixture{{owner: "octocat", repo: "public-repo"}})),
		}
		handler := serverTool.Handler(deps)

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

	t.Run("insiders mode all public emits public untrusted", func(t *testing.T) {
		searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{makeIssue("octocat", "public-repo", 1)}}
		deps := BaseDeps{
			Client:         mustNewGHClient(t, makeMockClient(searchResult, []repoFixture{{owner: "octocat", repo: "public-repo"}})),
			featureChecker: featureCheckerFor(FeatureFlagIFCLabels),
		}
		handler := serverTool.Handler(deps)

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

		require.NotNil(t, result.Meta)
		ifcMap := unmarshalIFC(t, result.Meta["ifc"])
		assert.Equal(t, "untrusted", ifcMap["integrity"])
		assert.Equal(t, "public", ifcMap["confidentiality"])
	})

	t.Run("insiders mode mixed public and private emits private untrusted", func(t *testing.T) {
		searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{
			makeIssue("octocat", "private-repo", 1),
			makeIssue("octocat", "public-repo", 2),
		}}
		deps := BaseDeps{
			Client: mustNewGHClient(t, makeMockClient(searchResult, []repoFixture{
				{owner: "octocat", repo: "private-repo", isPrivate: true},
				{owner: "octocat", repo: "public-repo"},
			})),
			featureChecker: featureCheckerFor(FeatureFlagIFCLabels),
		}
		handler := serverTool.Handler(deps)

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

		require.NotNil(t, result.Meta)
		ifcMap := unmarshalIFC(t, result.Meta["ifc"])
		assert.Equal(t, "untrusted", ifcMap["integrity"])
		assert.Equal(t, "private", ifcMap["confidentiality"])
	})

	t.Run("insiders mode skips ifc label when visibility lookup fails", func(t *testing.T) {
		searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{makeIssue("octocat", "broken", 1)}}
		deps := BaseDeps{
			Client: mustNewGHClient(t, makeMockClient(searchResult, []repoFixture{
				{owner: "octocat", repo: "broken", repoStatus: http.StatusInternalServerError},
			})),
			featureChecker: featureCheckerFor(FeatureFlagIFCLabels),
		}
		handler := serverTool.Handler(deps)

		request := createMCPRequest(reqParams)
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		require.False(t, result.IsError, "tool call should still succeed when visibility lookup fails")

		if result.Meta != nil {
			_, hasIFC := result.Meta["ifc"]
			assert.False(t, hasIFC, "ifc label should be omitted when visibility lookup fails")
		}
	})

	t.Run("insiders mode empty results emits public untrusted", func(t *testing.T) {
		searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{}}
		deps := BaseDeps{
			Client:         mustNewGHClient(t, makeMockClient(searchResult, nil)),
			featureChecker: featureCheckerFor(FeatureFlagIFCLabels),
		}
		handler := serverTool.Handler(deps)

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

		require.NotNil(t, result.Meta)
		ifcMap := unmarshalIFC(t, result.Meta["ifc"])
		assert.Equal(t, "untrusted", ifcMap["integrity"])
		assert.Equal(t, "public", ifcMap["confidentiality"])
	})
}

func unmarshalIFC(t *testing.T, ifcLabel any) map[string]any {
	t.Helper()
	require.NotNil(t, ifcLabel, "ifc label should be present")
	ifcJSON, err := json.Marshal(ifcLabel)
	require.NoError(t, err)
	var ifcMap map[string]any
	require.NoError(t, json.Unmarshal(ifcJSON, &ifcMap))
	return ifcMap
}

func Test_SearchIssues_FieldValuesEnrichment(t *testing.T) {
	serverTool := SearchIssues(translations.NullTranslationHelper)

	mockSearchResult := &github.IssuesSearchResult{
		Total:             github.Ptr(2),
		IncompleteResults: github.Ptr(false),
		Issues: []*github.Issue{
			{
				Number:  github.Ptr(42),
				Title:   github.Ptr("Bug: Something is broken"),
				State:   github.Ptr("open"),
				HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"),
				NodeID:  github.Ptr("I_node_42"),
				User:    &github.User{Login: github.Ptr("user1")},
			},
			{
				Number:  github.Ptr(43),
				Title:   github.Ptr("Feature request"),
				State:   github.Ptr("open"),
				HTMLURL: github.Ptr("https://github.com/owner/repo/issues/43"),
				NodeID:  github.Ptr("I_node_43"),
				User:    &github.User{Login: github.Ptr("user2")},
			},
		},
	}

	restClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		GetSearchIssues: mockResponse(t, http.StatusOK, mockSearchResult),
	})

	gqlVars := map[string]any{
		"ids": []any{"I_node_42", "I_node_43"},
	}
	gqlResponse := githubv4mock.DataResponse(map[string]any{
		"nodes": []map[string]any{
			{
				"id": "I_node_42",
				"issueFieldValues": map[string]any{
					"nodes": []map[string]any{
						{
							"__typename": "IssueFieldSingleSelectValue",
							"field":      map[string]any{"name": "priority"},
							"value":      "P1",
						},
						{
							"__typename":  "IssueFieldNumberValue",
							"field":       map[string]any{"name": "estimate"},
							"valueNumber": 2.5,
						},
					},
				},
			},
			{
				"id": "I_node_43",
				"issueFieldValues": map[string]any{
					"nodes": []map[string]any{},
				},
			},
		},
	})

	const nodesQueryString = "query($ids:[ID!]!){nodes(ids: $ids){... on Issue{id,issueFieldValues(first: 25){nodes{__typename,... on IssueFieldDateValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldNumberValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},valueNumber: value},... on IssueFieldSingleSelectValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldTextValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value}}}}}}"
	matcher := githubv4mock.NewQueryMatcher(nodesQueryString, gqlVars, gqlResponse)
	gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))

	deps := BaseDeps{
		Client:    mustNewGHClient(t, restClient),
		GQLClient: gqlClient,
	}
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"query": "repo:owner/repo is:open",
	})

	result, err := handler(ContextWithDeps(context.Background(), deps), &request)
	require.NoError(t, err)
	require.False(t, result.IsError, "expected result to not be an error")

	textContent := getTextResult(t, result)

	var response SearchIssuesResponse
	require.NoError(t, json.Unmarshal([]byte(textContent.Text), &response))
	require.Equal(t, 2, *response.Total)
	require.Len(t, response.Items, 2)
	assert.Equal(t, 42, *response.Items[0].Number)
	assert.Equal(t, []MinimalFieldValue{
		{Field: "priority", Value: "P1"},
		{Field: "estimate", Value: "2.5"},
	}, response.Items[0].FieldValues)
	assert.Equal(t, 43, *response.Items[1].Number)
	assert.Empty(t, response.Items[1].FieldValues)
}

func Test_CreateIssue(t *testing.T) {
	// Verify tool definition once
	serverTool := IssueWrite(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))
	require.Empty(t, serverTool.FeatureFlagEnable)

	assert.Equal(t, "issue_write", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "method")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "title")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "body")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "assignees")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "labels")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "milestone")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "type")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_fields")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo"})

	// Setup mock issue for success case
	mockIssue := &github.Issue{
		Number:    github.Ptr(123),
		Title:     github.Ptr("Test Issue"),
		Body:      github.Ptr("This is a test issue"),
		State:     github.Ptr("open"),
		HTMLURL:   github.Ptr("https://github.com/owner/repo/issues/123"),
		Assignees: []*github.User{{Login: github.Ptr("user1")}, {Login: github.Ptr("user2")}},
		Labels:    []*github.Label{{Name: github.Ptr("bug")}, {Name: github.Ptr("help wanted")}},
		Milestone: &github.Milestone{Number: github.Ptr(5)},
		Type:      &github.IssueType{Name: github.Ptr("Bug")},
	}

	tests := []struct {
		name            string
		mockedClient    *http.Client
		mockedGQLClient *http.Client
		requestArgs     map[string]any
		expectError     bool
		expectedIssue   *github.Issue
		expectedErrMsg  string
	}{
		{
			name: "successful issue creation with all fields",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesByOwnerByRepo: expectRequestBody(t, map[string]any{
					"title":     "Test Issue",
					"body":      "This is a test issue",
					"labels":    []any{"bug", "help wanted"},
					"assignees": []any{"user1", "user2"},
					"milestone": float64(5),
					"type":      "Bug",
				}).andThen(
					mockResponse(t, http.StatusCreated, mockIssue),
				),
			}),
			requestArgs: map[string]any{
				"method":    "create",
				"owner":     "owner",
				"repo":      "repo",
				"title":     "Test Issue",
				"body":      "This is a test issue",
				"assignees": []any{"user1", "user2"},
				"labels":    []any{"bug", "help wanted"},
				"milestone": float64(5),
				"type":      "Bug",
			},
			expectError:   false,
			expectedIssue: mockIssue,
		},
		{
			name: "successful issue creation with minimal fields",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesByOwnerByRepo: mockResponse(t, http.StatusCreated, &github.Issue{
					Number:  github.Ptr(124),
					Title:   github.Ptr("Minimal Issue"),
					HTMLURL: github.Ptr("https://github.com/owner/repo/issues/124"),
					State:   github.Ptr("open"),
				}),
			}),
			requestArgs: map[string]any{
				"method":    "create",
				"owner":     "owner",
				"repo":      "repo",
				"title":     "Minimal Issue",
				"assignees": nil, // Expect no failure with nil optional value.
			},
			expectError: false,
			expectedIssue: &github.Issue{
				Number:  github.Ptr(124),
				Title:   github.Ptr("Minimal Issue"),
				HTMLURL: github.Ptr("https://github.com/owner/repo/issues/124"),
				State:   github.Ptr("open"),
			},
		},
		{
			name: "successful issue creation with issue fields reconciled by names",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesByOwnerByRepo: expectRequestBody(t, map[string]any{
					"title":     "Issue with fields",
					"body":      "",
					"labels":    []any{},
					"assignees": []any{},
					"issue_field_values": []any{
						map[string]any{"field_id": float64(101), "value": "P1"},
						map[string]any{"field_id": float64(102), "value": "Acme"},
					},
				}).andThen(
					mockResponse(t, http.StatusCreated, &github.Issue{
						Number:  github.Ptr(125),
						Title:   github.Ptr("Issue with fields"),
						HTMLURL: github.Ptr("https://github.com/owner/repo/issues/125"),
						State:   github.Ptr("open"),
					}),
				),
			}),
			mockedGQLClient: githubv4mock.NewMockedHTTPClient(
				githubv4mock.NewQueryMatcher(
					issueFieldWriteMetadataQuery{},
					map[string]any{
						"owner": githubv4.String("owner"),
						"repo":  githubv4.String("repo"),
					},
					githubv4mock.DataResponse(map[string]any{
						"repository": map[string]any{
							"issueFields": map[string]any{
								"nodes": []any{
									map[string]any{
										"__typename":     "IssueFieldSingleSelect",
										"fullDatabaseId": "101",
										"name":           "Priority",
										"dataType":       "single_select",
										"options": []any{
											map[string]any{"fullDatabaseId": "9001", "name": "P1"},
										},
									},
									map[string]any{
										"__typename":     "IssueFieldText",
										"fullDatabaseId": "102",
										"name":           "Customer",
										"dataType":       "text",
									},
								},
							},
						},
					}),
				),
			),
			requestArgs: map[string]any{
				"method": "create",
				"owner":  "owner",
				"repo":   "repo",
				"title":  "Issue with fields",
				"issue_fields": []any{
					map[string]any{"field_name": "Priority", "field_option_name": "P1"},
					map[string]any{"field_name": "Customer", "value": "Acme"},
				},
			},
			expectError: false,
			expectedIssue: &github.Issue{
				Number:  github.Ptr(125),
				Title:   github.Ptr("Issue with fields"),
				HTMLURL: github.Ptr("https://github.com/owner/repo/issues/125"),
				State:   github.Ptr("open"),
			},
		},
		{
			name: "issue creation fails",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesByOwnerByRepo: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusUnprocessableEntity)
					_, _ = w.Write([]byte(`{"message": "Validation failed"}`))
				}),
			}),
			requestArgs: map[string]any{
				"method": "create",
				"owner":  "owner",
				"repo":   "repo",
				"title":  "",
			},
			expectError:    false,
			expectedErrMsg: "missing required parameter: title",
		},
		{
			name:         "issue_fields rejects both value and field_option_name",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"method": "create",
				"owner":  "owner",
				"repo":   "repo",
				"title":  "Invalid fields",
				"issue_fields": []any{
					map[string]any{"field_name": "Priority", "value": "P1", "field_option_name": "P1"},
				},
			},
			expectError:    false,
			expectedErrMsg: "cannot specify both value and field_option_name",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Setup client with mock
			client := mustNewGHClient(t, tc.mockedClient)
			gqlHTTPClient := tc.mockedGQLClient
			if gqlHTTPClient == nil {
				gqlHTTPClient = githubv4mock.NewMockedHTTPClient()
			}
			gqlClient := githubv4.NewClient(gqlHTTPClient)
			deps := BaseDeps{
				Client:    client,
				GQLClient: gqlClient,
			}
			handler := serverTool.Handler(deps)

			// Create call request
			request := createMCPRequest(tc.requestArgs)

			// Call handler
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)

			// Verify results
			if tc.expectError {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.expectedErrMsg)
				return
			}

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

			require.NoError(t, err)
			textContent := getTextResult(t, result)

			// Unmarshal and verify the minimal result
			var returnedIssue MinimalResponse
			err = json.Unmarshal([]byte(textContent.Text), &returnedIssue)
			require.NoError(t, err)

			assert.Equal(t, tc.expectedIssue.GetHTMLURL(), returnedIssue.URL)
		})
	}
}

// Test_IssueWrite_MCPAppsFeature_UIGate verifies the MCP Apps feature UI gate
// behavior: UI clients get a form message, non-UI clients execute directly.
func Test_IssueWrite_MCPAppsFeature_UIGate(t *testing.T) {
	t.Parallel()

	mockIssue := &github.Issue{
		Number:  github.Ptr(1),
		Title:   github.Ptr("Test"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/1"),
	}

	serverTool := IssueWrite(translations.NullTranslationHelper)

	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		PostReposIssuesByOwnerByRepo: mockResponse(t, http.StatusCreated, mockIssue),
	}))

	deps := BaseDeps{
		Client:         client,
		GQLClient:      githubv4.NewClient(nil),
		featureChecker: featureCheckerFor(MCPAppsFeatureFlag),
	}
	handler := serverTool.Handler(deps)

	t.Run("UI client without _ui_submitted returns form message", func(t *testing.T) {
		request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
			"method": "create",
			"owner":  "owner",
			"repo":   "repo",
			"title":  "Test",
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)

		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "interactive form has been shown to the user for creating a new issue")
		assert.True(t, result.IsError, "form-routing stub should be marked IsError so agents don't claim success")
	})

	t.Run("UI client with _ui_submitted executes directly", func(t *testing.T) {
		request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
			"method":        "create",
			"owner":         "owner",
			"repo":          "repo",
			"title":         "Test",
			"_ui_submitted": true,
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)

		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
			"tool should return the created issue URL")
	})

	t.Run("non-UI client executes directly without _ui_submitted", func(t *testing.T) {
		request := createMCPRequest(map[string]any{
			"method": "create",
			"owner":  "owner",
			"repo":   "repo",
			"title":  "Test",
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)

		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "https://github.com/owner/repo/issues/1",
			"non-UI client should execute directly")
	})

	t.Run("UI client with state change routes through UI form", func(t *testing.T) {
		// state/state_reason/duplicate_of are form params (the issue-write view
		// renders close/reopen controls), so a call carrying them must go to
		// the form rather than execute directly.
		request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
			"method":       "update",
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(1),
			"state":        "closed",
			"state_reason": "completed",
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)

		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "interactive form has been shown to the user for editing issue #1",
			"state change should route through UI form")
		assert.True(t, result.IsError, "form-routing stub should be marked IsError so agents don't claim success")
	})

	t.Run("UI client update without state change returns form message", func(t *testing.T) {
		request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
			"method":       "update",
			"owner":        "owner",
			"repo":         "repo",
			"issue_number": float64(1),
			"title":        "New Title",
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)

		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "interactive form has been shown to the user for editing issue #1",
			"update without state should show UI form")
		assert.True(t, result.IsError, "form-routing stub should be marked IsError so agents don't claim success")
	})

	t.Run("UI client with issue_fields routes through UI form", func(t *testing.T) {
		// issue_fields is now a form param (the issue-write view renders a
		// per-field editor), so a call carrying it must go to the form rather
		// than execute directly.
		request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
			"method": "create",
			"owner":  "owner",
			"repo":   "repo",
			"title":  "Issue with fields",
			"issue_fields": []any{
				map[string]any{"field_name": "Priority", "field_option_name": "P1"},
			},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)

		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "interactive form has been shown to the user for creating a new issue",
			"issue_fields should route through UI form")
		assert.True(t, result.IsError, "form-routing stub should be marked IsError so agents don't claim success")
	})

	t.Run("UI client with labels routes through UI form", func(t *testing.T) {
		// labels is now a form param (the issue-write view prefills and renders
		// a label selector), so a call carrying them must route to the form
		// rather than execute directly.
		request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
			"method": "create",
			"owner":  "owner",
			"repo":   "repo",
			"title":  "Test",
			"labels": []any{"bug"},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)

		textContent := getTextResult(t, result)
		assert.Contains(t, textContent.Text, "interactive form has been shown to the user for creating a new issue",
			"labels should route through UI form")
		assert.True(t, result.IsError, "form-routing stub should be marked IsError so agents don't claim success")
	})
}

func Test_issueWriteHasNonFormParams(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name string
		args map[string]any
		want bool
	}{
		{name: "no params", args: map[string]any{}, want: false},
		{name: "only form params", args: map[string]any{"method": "create", "owner": "o", "repo": "r", "title": "t", "body": "b", "issue_number": float64(1), "_ui_submitted": true}, want: false},
		{name: "labels present", args: map[string]any{"title": "t", "labels": []any{"bug"}}, want: false},
		{name: "assignees present", args: map[string]any{"title": "t", "assignees": []any{"octocat"}}, want: false},
		{name: "milestone present", args: map[string]any{"title": "t", "milestone": float64(2)}, want: false},
		{name: "type present", args: map[string]any{"title": "t", "type": "Bug"}, want: false},
		{name: "issue_fields present", args: map[string]any{"issue_fields": []any{map[string]any{"field_name": "Priority"}}}, want: false},
		{name: "state present", args: map[string]any{"state": "closed"}, want: false},
		{name: "state_reason present", args: map[string]any{"state_reason": "completed"}, want: false},
		{name: "duplicate_of present", args: map[string]any{"duplicate_of": float64(7)}, want: false},
		{name: "unknown non-schema param present", args: map[string]any{"title": "t", "not_a_real_param": "x"}, want: true},
		{name: "nil value is ignored", args: map[string]any{"issue_fields": nil}, want: false},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			assert.Equal(t, tc.want, hasNonFormParams(tc.args, issueWriteFormParams))
		})
	}
}

// Test_issueWriteSchemaClassification fails when a schema property is added
// without classifying it as either form-resendable (issueWriteFormParams) or
// known-non-form (knownNonForm below). Without this guard, an unclassified
// property would silently flip UI gating: form-incompatible fields would
// stop tripping the safety-net bypass and the form would drop their values.
func Test_issueWriteSchemaClassification(t *testing.T) {
	t.Parallel()

	// Schema properties the MCP App form cannot represent — their presence
	// must trigger the safety-net bypass via hasNonFormParams. The
	// form currently collects every schema property, so this allowlist is
	// empty; add a property here only if it is added to the schema without
	// corresponding form support.
	knownNonForm := map[string]struct{}{}

	cases := []struct {
		name string
		tool inventory.ServerTool
	}{
		{name: "IssueWrite", tool: IssueWrite(translations.NullTranslationHelper)},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			schema, ok := tc.tool.Tool.InputSchema.(*jsonschema.Schema)
			require.True(t, ok, "InputSchema should be *jsonschema.Schema")

			for prop := range schema.Properties {
				_, isForm := issueWriteFormParams[prop]
				_, isNonForm := knownNonForm[prop]

				assert.Falsef(t, isForm && isNonForm,
					"property %q is classified as both form-resendable and non-form — pick one", prop)
				assert.Truef(t, isForm || isNonForm,
					"property %q in %s schema is unclassified — add it to issueWriteFormParams (pkg/github/issues.go) "+
						"if the MCP App form can carry it on submit, otherwise add it to the knownNonForm allowlist in this test",
					prop, tc.name)
			}
		})
	}
}

func Test_ListIssues(t *testing.T) {
	// Verify tool definition
	serverTool := ListIssues(translations.NullTranslationHelper)
	tool := serverTool.Tool
	// ListIssues is the FeatureFlagFieldsParam-enabled variant; it owns the
	// _ff_<flag> snapshot. The canonical list_issues.snap is owned by
	// LegacyListIssues (see Test_LegacyListIssues_Definition).
	require.NoError(t, toolsnaps.Test(tool.Name+"_ff_"+FeatureFlagFieldsParam, tool))
	require.Equal(t, FeatureFlagFieldsParam, serverTool.FeatureFlagEnable)

	assert.Equal(t, "list_issues", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "state")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "labels")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "orderBy")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "direction")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "since")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "after")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "perPage")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "fields")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo"})

	// Mock issues data
	mockIssuesAll := []map[string]any{
		{
			"number":     123,
			"title":      "First Issue",
			"body":       "This is the first test issue",
			"state":      "OPEN",
			"databaseId": 1001,
			"createdAt":  "2023-01-01T00:00:00Z",
			"updatedAt":  "2023-01-01T00:00:00Z",
			"author":     map[string]any{"login": "user1"},
			"labels": map[string]any{
				"nodes": []map[string]any{
					{"name": "bug", "id": "label1", "description": "Bug label"},
				},
			},
			"comments": map[string]any{
				"totalCount": 5,
			},
			"issueFieldValues": map[string]any{
				"nodes": []map[string]any{
					{
						"__typename": "IssueFieldSingleSelectValue",
						"field":      map[string]any{"name": "priority"},
						"value":      "P1",
					},
				},
			},
		},
		{
			"number":     456,
			"title":      "Second Issue",
			"body":       "This is the second test issue",
			"state":      "OPEN",
			"databaseId": 1002,
			"createdAt":  "2023-02-01T00:00:00Z",
			"updatedAt":  "2023-02-01T00:00:00Z",
			"author":     map[string]any{"login": "user2"},
			"labels": map[string]any{
				"nodes": []map[string]any{
					{"name": "enhancement", "id": "label2", "description": "Enhancement label"},
				},
			},
			"comments": map[string]any{
				"totalCount": 3,
			},
			"issueFieldValues": map[string]any{
				"nodes": []map[string]any{
					{
						"__typename": "IssueFieldDateValue",
						"field":      map[string]any{"name": "due"},
						"value":      "2026-06-01",
					},
					{
						"__typename":  "IssueFieldNumberValue",
						"field":       map[string]any{"name": "estimate"},
						"valueNumber": 2.5,
					},
					{
						"__typename": "IssueFieldTextValue",
						"field":      map[string]any{"name": "notes"},
						"value":      "needs triage",
					},
				},
			},
		},
	}

	mockIssuesOpen := []map[string]any{mockIssuesAll[0], mockIssuesAll[1]}
	mockIssuesClosed := []map[string]any{
		{
			"number":     789,
			"title":      "Closed Issue",
			"body":       "This is a closed issue",
			"state":      "CLOSED",
			"databaseId": 1003,
			"createdAt":  "2023-03-01T00:00:00Z",
			"updatedAt":  "2023-03-01T00:00:00Z",
			"author":     map[string]any{"login": "user3"},
			"labels": map[string]any{
				"nodes": []map[string]any{},
			},
			"comments": map[string]any{
				"totalCount": 1,
			},
			"issueFieldValues": map[string]any{
				"nodes": []map[string]any{},
			},
		},
	}

	// Mock responses
	mockResponseListAll := githubv4mock.DataResponse(map[string]any{
		"repository": map[string]any{
			"issues": map[string]any{
				"nodes": mockIssuesAll,
				"pageInfo": map[string]any{
					"hasNextPage":     false,
					"hasPreviousPage": false,
					"startCursor":     "",
					"endCursor":       "",
				},
				"totalCount": 2,
			},
			"isPrivate": false,
		},
	})

	mockResponseOpenOnly := githubv4mock.DataResponse(map[string]any{
		"repository": map[string]any{
			"issues": map[string]any{
				"nodes": mockIssuesOpen,
				"pageInfo": map[string]any{
					"hasNextPage":     false,
					"hasPreviousPage": false,
					"startCursor":     "",
					"endCursor":       "",
				},
				"totalCount": 2,
			},
			"isPrivate": false,
		},
	})

	mockResponseClosedOnly := githubv4mock.DataResponse(map[string]any{
		"repository": map[string]any{
			"issues": map[string]any{
				"nodes": mockIssuesClosed,
				"pageInfo": map[string]any{
					"hasNextPage":     false,
					"hasPreviousPage": false,
					"startCursor":     "",
					"endCursor":       "",
				},
				"totalCount": 1,
			},
			"isPrivate": false,
		},
	})

	mockErrorRepoNotFound := githubv4mock.ErrorResponse("repository not found")

	// Variables matching what GraphQL receives after JSON marshaling/unmarshaling.
	// issueFieldValues is always sent as an (empty by default) list because the query
	// declares the variable unconditionally; the server treats an empty list as no filter.
	varsListAll := map[string]any{
		"owner":            "owner",
		"repo":             "repo",
		"states":           []any{"OPEN", "CLOSED"},
		"orderBy":          "CREATED_AT",
		"direction":        "DESC",
		"first":            float64(30),
		"after":            (*string)(nil),
		"issueFieldValues": []any{},
	}

	varsOpenOnly := map[string]any{
		"owner":            "owner",
		"repo":             "repo",
		"states":           []any{"OPEN"},
		"orderBy":          "CREATED_AT",
		"direction":        "DESC",
		"first":            float64(30),
		"after":            (*string)(nil),
		"issueFieldValues": []any{},
	}

	varsClosedOnly := map[string]any{
		"owner":            "owner",
		"repo":             "repo",
		"states":           []any{"CLOSED"},
		"orderBy":          "CREATED_AT",
		"direction":        "DESC",
		"first":            float64(30),
		"after":            (*string)(nil),
		"issueFieldValues": []any{},
	}

	varsWithLabels := map[string]any{
		"owner":            "owner",
		"repo":             "repo",
		"states":           []any{"OPEN", "CLOSED"},
		"labels":           []any{"bug", "enhancement"},
		"orderBy":          "CREATED_AT",
		"direction":        "DESC",
		"first":            float64(30),
		"after":            (*string)(nil),
		"issueFieldValues": []any{},
	}

	varsRepoNotFound := map[string]any{
		"owner":            "owner",
		"repo":             "nonexistent-repo",
		"states":           []any{"OPEN", "CLOSED"},
		"orderBy":          "CREATED_AT",
		"direction":        "DESC",
		"first":            float64(30),
		"after":            (*string)(nil),
		"issueFieldValues": []any{},
	}

	tests := []struct {
		name          string
		reqParams     map[string]any
		expectError   bool
		errContains   string
		expectedCount int
	}{
		{
			name: "list all issues",
			reqParams: map[string]any{
				"owner": "owner",
				"repo":  "repo",
			},
			expectError:   false,
			expectedCount: 2,
		},
		{
			name: "filter by open state",
			reqParams: map[string]any{
				"owner": "owner",
				"repo":  "repo",
				"state": "OPEN",
			},
			expectError:   false,
			expectedCount: 2,
		},
		{
			name: "filter by open state - lc",
			reqParams: map[string]any{
				"owner": "owner",
				"repo":  "repo",
				"state": "open",
			},
			expectError:   false,
			expectedCount: 2,
		},
		{
			name: "filter by closed state",
			reqParams: map[string]any{
				"owner": "owner",
				"repo":  "repo",
				"state": "CLOSED",
			},
			expectError:   false,
			expectedCount: 1,
		},
		{
			name: "filter by labels",
			reqParams: map[string]any{
				"owner":  "owner",
				"repo":   "repo",
				"labels": []any{"bug", "enhancement"},
			},
			expectError:   false,
			expectedCount: 2,
		},
		{
			name: "repository not found error",
			reqParams: map[string]any{
				"owner": "owner",
				"repo":  "nonexistent-repo",
			},
			expectError: true,
			errContains: "repository not found",
		},
	}

	// Define the actual query strings that match the implementation
	issueFieldValuesSelection := "issueFieldValues(first: 25){nodes{__typename,... on IssueFieldDateValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldNumberValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},valueNumber: value},... on IssueFieldSingleSelectValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldTextValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value}}}"
	qBasicNoLabels := "query($after:String$direction:OrderDirection!$first:Int!$issueFieldValues:[IssueFieldValueFilter!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {issueFieldValues: $issueFieldValues}){nodes{number,title,body,state,databaseId,author{login},createdAt,updatedAt,labels(first: 100){nodes{name,id,description}},comments{totalCount}," + issueFieldValuesSelection + "},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount},isPrivate}}"
	qWithLabels := "query($after:String$direction:OrderDirection!$first:Int!$issueFieldValues:[IssueFieldValueFilter!]!$labels:[String!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, labels: $labels, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {issueFieldValues: $issueFieldValues}){nodes{number,title,body,state,databaseId,author{login},createdAt,updatedAt,labels(first: 100){nodes{name,id,description}},comments{totalCount}," + issueFieldValuesSelection + "},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount},isPrivate}}"

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			var httpClient *http.Client

			switch tc.name {
			case "list all issues":
				matcher := githubv4mock.NewQueryMatcher(qBasicNoLabels, varsListAll, mockResponseListAll)
				httpClient = githubv4mock.NewMockedHTTPClient(matcher)
			case "filter by open state":
				matcher := githubv4mock.NewQueryMatcher(qBasicNoLabels, varsOpenOnly, mockResponseOpenOnly)
				httpClient = githubv4mock.NewMockedHTTPClient(matcher)
			case "filter by open state - lc":
				matcher := githubv4mock.NewQueryMatcher(qBasicNoLabels, varsOpenOnly, mockResponseOpenOnly)
				httpClient = githubv4mock.NewMockedHTTPClient(matcher)
			case "filter by closed state":
				matcher := githubv4mock.NewQueryMatcher(qBasicNoLabels, varsClosedOnly, mockResponseClosedOnly)
				httpClient = githubv4mock.NewMockedHTTPClient(matcher)
			case "filter by labels":
				matcher := githubv4mock.NewQueryMatcher(qWithLabels, varsWithLabels, mockResponseListAll)
				httpClient = githubv4mock.NewMockedHTTPClient(matcher)
			case "repository not found error":
				matcher := githubv4mock.NewQueryMatcher(qBasicNoLabels, varsRepoNotFound, mockErrorRepoNotFound)
				httpClient = githubv4mock.NewMockedHTTPClient(matcher)
			}

			gqlClient := githubv4.NewClient(httpClient)
			deps := BaseDeps{
				GQLClient: gqlClient,
			}
			handler := serverTool.Handler(deps)

			req := createMCPRequest(tc.reqParams)
			res, err := handler(ContextWithDeps(context.Background(), deps), &req)
			text := getTextResult(t, res).Text

			if tc.expectError {
				require.True(t, res.IsError)
				assert.Contains(t, text, tc.errContains)
				return
			}
			require.NoError(t, err)

			// Parse the structured response with pagination info
			var response MinimalIssuesResponse
			err = json.Unmarshal([]byte(text), &response)
			require.NoError(t, err)

			assert.Len(t, response.Issues, tc.expectedCount, "Expected %d issues, got %d", tc.expectedCount, len(response.Issues))

			// Verify pagination metadata
			assert.Equal(t, tc.expectedCount, response.TotalCount)
			assert.False(t, response.PageInfo.HasNextPage)
			assert.False(t, response.PageInfo.HasPreviousPage)

			// Verify that returned issues have expected structure
			for _, issue := range response.Issues {
				assert.NotZero(t, issue.Number, "Issue should have number")
				assert.NotEmpty(t, issue.Title, "Issue should have title")
				assert.NotEmpty(t, issue.State, "Issue should have state")
				assert.NotEmpty(t, issue.CreatedAt, "Issue should have created_at")
				assert.NotEmpty(t, issue.UpdatedAt, "Issue should have updated_at")
				assert.NotNil(t, issue.User, "Issue should have user")
				assert.NotEmpty(t, issue.User.Login, "Issue user should have login")
				assert.Empty(t, issue.HTMLURL, "html_url should be empty (not populated by GraphQL fragment)")

				// Labels should be flattened to name strings
				for _, label := range issue.Labels {
					assert.NotEmpty(t, label, "Label should be a non-empty string")
				}

				// Field values should be flattened to {field, value} pairs. Issue #123 has a
				// SingleSelectValue; issue #456 exercises the Date/Number/Text branches
				// (including float formatting); #789 has no field values.
				switch issue.Number {
				case 123:
					assert.Equal(t, []MinimalFieldValue{{Field: "priority", Value: "P1"}}, issue.FieldValues)
				case 456:
					assert.Equal(t, []MinimalFieldValue{
						{Field: "due", Value: "2026-06-01"},
						{Field: "estimate", Value: "2.5"},
						{Field: "notes", Value: "needs triage"},
					}, issue.FieldValues)
				default:
					assert.Empty(t, issue.FieldValues)
				}
			}
		})
	}
}

func Test_ListIssues_FieldFilters(t *testing.T) {
	t.Parallel()

	serverTool := ListIssues(translations.NullTranslationHelper)

	mockIssues := []map[string]any{
		{
			"number":     1,
			"title":      "An issue",
			"body":       "body",
			"state":      "OPEN",
			"databaseId": 1,
			"createdAt":  "2026-01-01T00:00:00Z",
			"updatedAt":  "2026-01-01T00:00:00Z",
			"author":     map[string]any{"login": "user1"},
			"labels":     map[string]any{"nodes": []map[string]any{}},
			"comments":   map[string]any{"totalCount": 0},
		},
	}

	pageInfo := map[string]any{
		"hasNextPage":     false,
		"hasPreviousPage": false,
		"startCursor":     "",
		"endCursor":       "",
	}

	response := githubv4mock.DataResponse(map[string]any{
		"repository": map[string]any{
			"issues": map[string]any{
				"nodes":      mockIssues,
				"pageInfo":   pageInfo,
				"totalCount": 1,
			},
			"isPrivate": false,
		},
	})

	// Field-lookup matcher used by every subtest that supplies field_filters.
	// The handler calls fetchIssueFields(owner, repo) before issuing the issues query.
	fieldsResponse := githubv4mock.DataResponse(map[string]any{
		"repository": map[string]any{
			"issueFields": map[string]any{
				"nodes": []any{
					map[string]any{
						"__typename": "IssueFieldSingleSelect",
						"id":         "IFSS_1",
						"name":       "Priority",
						"dataType":   "SINGLE_SELECT",
						"visibility": "ALL",
						"options": []any{
							map[string]any{"id": "OPT_P1", "name": "P1", "color": "red"},
							map[string]any{"id": "OPT_P2", "name": "P2", "color": "yellow"},
						},
					},
					map[string]any{
						"__typename": "IssueFieldText",
						"id":         "IFT_1",
						"name":       "Notes",
						"dataType":   "TEXT",
						"visibility": "ALL",
					},
					map[string]any{
						"__typename": "IssueFieldNumber",
						"id":         "IFN_1",
						"name":       "Estimate",
						"dataType":   "NUMBER",
						"visibility": "ALL",
					},
					map[string]any{
						"__typename": "IssueFieldDate",
						"id":         "IFD_1",
						"name":       "Due",
						"dataType":   "DATE",
						"visibility": "ALL",
					},
				},
			},
		},
	})
	fieldsMatcher := func() githubv4mock.Matcher {
		return githubv4mock.NewQueryMatcher(
			issueFieldsRepoQuery{},
			map[string]any{
				"owner": githubv4.String("owner"),
				"name":  githubv4.String("repo"),
			},
			fieldsResponse,
		)
	}

	qNoLabels := "query($after:String$direction:OrderDirection!$first:Int!$issueFieldValues:[IssueFieldValueFilter!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {issueFieldValues: $issueFieldValues}){nodes{number,title,body,state,databaseId,author{login},createdAt,updatedAt,labels(first: 100){nodes{name,id,description}},comments{totalCount},issueFieldValues(first: 25){nodes{__typename,... on IssueFieldDateValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldNumberValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},valueNumber: value},... on IssueFieldSingleSelectValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldTextValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value}}}},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount},isPrivate}}"
	qWithLabels := "query($after:String$direction:OrderDirection!$first:Int!$issueFieldValues:[IssueFieldValueFilter!]!$labels:[String!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, labels: $labels, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {issueFieldValues: $issueFieldValues}){nodes{number,title,body,state,databaseId,author{login},createdAt,updatedAt,labels(first: 100){nodes{name,id,description}},comments{totalCount},issueFieldValues(first: 25){nodes{__typename,... on IssueFieldDateValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldNumberValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},valueNumber: value},... on IssueFieldSingleSelectValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldTextValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value}}}},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount},isPrivate}}"

	baseVars := func() map[string]any {
		return map[string]any{
			"owner":     "owner",
			"repo":      "repo",
			"states":    []any{"OPEN", "CLOSED"},
			"orderBy":   "CREATED_AT",
			"direction": "DESC",
			"first":     float64(30),
			"after":     (*string)(nil),
		}
	}

	t.Run("single select field filter", func(t *testing.T) {
		vars := baseVars()
		vars["issueFieldValues"] = []any{
			map[string]any{"fieldName": "Priority", "singleSelectOptionValue": "P1"},
		}
		matcher := githubv4mock.NewQueryMatcher(qNoLabels, vars, response)
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(fieldsMatcher(), matcher))
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{
			"owner": "owner",
			"repo":  "repo",
			"field_filters": []any{
				map[string]any{"field_name": "Priority", "value": "P1"},
			},
		})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.False(t, res.IsError, getTextResult(t, res).Text)
	})

	t.Run("text field filter combined with labels", func(t *testing.T) {
		vars := baseVars()
		vars["labels"] = []any{"bug"}
		vars["issueFieldValues"] = []any{
			map[string]any{"fieldName": "Notes", "textValue": "needs triage"},
		}
		matcher := githubv4mock.NewQueryMatcher(qWithLabels, vars, response)
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(fieldsMatcher(), matcher))
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{
			"owner":  "owner",
			"repo":   "repo",
			"labels": []any{"bug"},
			"field_filters": []any{
				map[string]any{"field_name": "Notes", "value": "needs triage"},
			},
		})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.False(t, res.IsError, getTextResult(t, res).Text)
	})

	t.Run("number and date field filters", func(t *testing.T) {
		vars := baseVars()
		vars["issueFieldValues"] = []any{
			map[string]any{"fieldName": "Estimate", "numberValue": float64(2.5)},
			map[string]any{"fieldName": "Due", "dateValue": "2026-06-01"},
		}
		matcher := githubv4mock.NewQueryMatcher(qNoLabels, vars, response)
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(fieldsMatcher(), matcher))
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{
			"owner": "owner",
			"repo":  "repo",
			"field_filters": []any{
				map[string]any{"field_name": "Estimate", "value": "2.5"},
				map[string]any{"field_name": "Due", "value": "2026-06-01"},
			},
		})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.False(t, res.IsError, getTextResult(t, res).Text)
	})

	t.Run("number field accepts zero values", func(t *testing.T) {
		for _, value := range []string{"0", "0.0"} {
			t.Run(value, func(t *testing.T) {
				vars := baseVars()
				vars["issueFieldValues"] = []any{
					map[string]any{"fieldName": "Estimate", "numberValue": float64(0)},
				}
				matcher := githubv4mock.NewQueryMatcher(qNoLabels, vars, response)
				gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(fieldsMatcher(), matcher))
				deps := BaseDeps{GQLClient: gqlClient}
				handler := serverTool.Handler(deps)

				req := createMCPRequest(map[string]any{
					"owner": "owner",
					"repo":  "repo",
					"field_filters": []any{
						map[string]any{"field_name": "Estimate", "value": value},
					},
				})
				res, err := handler(ContextWithDeps(context.Background(), deps), &req)
				require.NoError(t, err)
				require.False(t, res.IsError, getTextResult(t, res).Text)
			})
		}
	})

	t.Run("validation error when value missing", func(t *testing.T) {
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(githubv4mock.NewQueryMatcher("", nil, response)))
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{
			"owner": "owner",
			"repo":  "repo",
			"field_filters": []any{
				map[string]any{"field_name": "Priority"},
			},
		})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.True(t, res.IsError)
		text := getTextResult(t, res).Text
		assert.Contains(t, text, "field_filters entry")
		assert.Contains(t, text, "Priority")
		assert.Contains(t, text, "value")
	})

	t.Run("validation error when field_name missing", func(t *testing.T) {
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(githubv4mock.NewQueryMatcher("", nil, response)))
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{
			"owner": "owner",
			"repo":  "repo",
			"field_filters": []any{
				map[string]any{"value": "P1"},
			},
		})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.True(t, res.IsError)
		text := getTextResult(t, res).Text
		assert.Contains(t, text, "field_filters entry")
		assert.Contains(t, text, "field_name")
	})

	t.Run("error when field is unknown", func(t *testing.T) {
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(fieldsMatcher()))
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{
			"owner": "owner",
			"repo":  "repo",
			"field_filters": []any{
				map[string]any{"field_name": "NotARealField", "value": "x"},
			},
		})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.True(t, res.IsError)
		text := getTextResult(t, res).Text
		assert.Contains(t, text, "unknown field")
		assert.Contains(t, text, "Priority")
	})

	t.Run("error when single-select option is invalid", func(t *testing.T) {
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(fieldsMatcher()))
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{
			"owner": "owner",
			"repo":  "repo",
			"field_filters": []any{
				map[string]any{"field_name": "Priority", "value": "P9"},
			},
		})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.True(t, res.IsError)
		text := getTextResult(t, res).Text
		assert.Contains(t, text, "not a valid option")
		assert.Contains(t, text, "P1")
	})

	t.Run("error when number value is non-numeric", func(t *testing.T) {
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(fieldsMatcher()))
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{
			"owner": "owner",
			"repo":  "repo",
			"field_filters": []any{
				map[string]any{"field_name": "Estimate", "value": "not-a-number"},
			},
		})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.True(t, res.IsError)
		assert.Contains(t, getTextResult(t, res).Text, "not a valid number")
	})

	t.Run("error when date value is malformed", func(t *testing.T) {
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(fieldsMatcher()))
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{
			"owner": "owner",
			"repo":  "repo",
			"field_filters": []any{
				map[string]any{"field_name": "Due", "value": "06/01/2026"},
			},
		})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.True(t, res.IsError)
		assert.Contains(t, getTextResult(t, res).Text, "not a valid date")
	})

	// Query string fragments for the `since` variants. Built by string concatenation
	// because they only differ from the base variants by the variable declaration and
	// the filterBy clause.
	qNoLabelsWithSince := "query($after:String$direction:OrderDirection!$first:Int!$issueFieldValues:[IssueFieldValueFilter!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$since:DateTime!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {since: $since, issueFieldValues: $issueFieldValues})" + qNoLabels[len("query($after:String$direction:OrderDirection!$first:Int!$issueFieldValues:[IssueFieldValueFilter!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {issueFieldValues: $issueFieldValues})"):]
	qLabelsWithSince := "query($after:String$direction:OrderDirection!$first:Int!$issueFieldValues:[IssueFieldValueFilter!]!$labels:[String!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$since:DateTime!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, labels: $labels, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {since: $since, issueFieldValues: $issueFieldValues})" + qWithLabels[len("query($after:String$direction:OrderDirection!$first:Int!$issueFieldValues:[IssueFieldValueFilter!]!$labels:[String!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, labels: $labels, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {issueFieldValues: $issueFieldValues})"):]

	t.Run("field filter with since", func(t *testing.T) {
		vars := baseVars()
		vars["since"] = "2026-01-01T00:00:00Z"
		vars["issueFieldValues"] = []any{
			map[string]any{"fieldName": "Priority", "singleSelectOptionValue": "P1"},
		}
		matcher := githubv4mock.NewQueryMatcher(qNoLabelsWithSince, vars, response)
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(fieldsMatcher(), matcher))
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{
			"owner": "owner",
			"repo":  "repo",
			"since": "2026-01-01T00:00:00Z",
			"field_filters": []any{
				map[string]any{"field_name": "Priority", "value": "P1"},
			},
		})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.False(t, res.IsError, getTextResult(t, res).Text)
	})

	t.Run("field filter with labels and since", func(t *testing.T) {
		vars := baseVars()
		vars["labels"] = []any{"bug"}
		vars["since"] = "2026-01-01T00:00:00Z"
		vars["issueFieldValues"] = []any{
			map[string]any{"fieldName": "Priority", "singleSelectOptionValue": "P1"},
		}
		matcher := githubv4mock.NewQueryMatcher(qLabelsWithSince, vars, response)
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(fieldsMatcher(), matcher))
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{
			"owner":  "owner",
			"repo":   "repo",
			"labels": []any{"bug"},
			"since":  "2026-01-01T00:00:00Z",
			"field_filters": []any{
				map[string]any{"field_name": "Priority", "value": "P1"},
			},
		})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.False(t, res.IsError, getTextResult(t, res).Text)
	})

	t.Run("sends GraphQL-Features: issue_fields, repo_issue_fields header", func(t *testing.T) {
		vars := baseVars()
		vars["issueFieldValues"] = []any{}
		matcher := githubv4mock.NewQueryMatcher(qNoLabels, vars, response)

		// Build a transport chain matching production: GraphQLFeaturesTransport
		// wraps a header-capturing spy, which forwards to the mock's RoundTripper.
		// This verifies the handler sets the issue_fields context value and the
		// transport translates it into the outgoing header.
		mockClient := githubv4mock.NewMockedHTTPClient(matcher)
		spy := &headerCaptureTransport{inner: mockClient.Transport}
		httpClient := &http.Client{
			Transport: &transportpkg.GraphQLFeaturesTransport{Transport: spy},
		}
		gqlClient := githubv4.NewClient(httpClient)
		deps := BaseDeps{GQLClient: gqlClient}
		handler := serverTool.Handler(deps)

		req := createMCPRequest(map[string]any{"owner": "owner", "repo": "repo"})
		res, err := handler(ContextWithDeps(context.Background(), deps), &req)
		require.NoError(t, err)
		require.False(t, res.IsError, getTextResult(t, res).Text)
		assert.Equal(t, "issue_fields, repo_issue_fields", spy.captured.Get(headers.GraphQLFeaturesHeader))
	})
}

// headerCaptureTransport records the headers of the most recent request that passed
// through it before forwarding to the inner RoundTripper.
type headerCaptureTransport struct {
	inner    http.RoundTripper
	captured http.Header
}

func (t *headerCaptureTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	t.captured = req.Header.Clone()
	return t.inner.RoundTrip(req)
}

func Test_ListIssues_IFC_InsidersMode(t *testing.T) {
	t.Parallel()

	serverTool := ListIssues(translations.NullTranslationHelper)

	mockIssues := []map[string]any{
		{
			"number":     1,
			"title":      "An issue",
			"body":       "body",
			"state":      "OPEN",
			"databaseId": 1,
			"createdAt":  "2023-01-01T00:00:00Z",
			"updatedAt":  "2023-01-01T00:00:00Z",
			"author":     map[string]any{"login": "user1"},
			"labels":     map[string]any{"nodes": []map[string]any{}},
			"comments":   map[string]any{"totalCount": 0},
		},
	}

	pageInfo := map[string]any{
		"hasNextPage":     false,
		"hasPreviousPage": false,
		"startCursor":     "",
		"endCursor":       "",
	}

	makeResponse := func(isPrivate bool) githubv4mock.GQLResponse {
		return githubv4mock.DataResponse(map[string]any{
			"repository": map[string]any{
				"issues": map[string]any{
					"nodes":      mockIssues,
					"pageInfo":   pageInfo,
					"totalCount": 1,
				},
				"isPrivate": isPrivate,
			},
		})
	}

	query := "query($after:String$direction:OrderDirection!$first:Int!$issueFieldValues:[IssueFieldValueFilter!]!$orderBy:IssueOrderField!$owner:String!$repo:String!$states:[IssueState!]!){repository(owner: $owner, name: $repo){issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {issueFieldValues: $issueFieldValues}){nodes{number,title,body,state,databaseId,author{login},createdAt,updatedAt,labels(first: 100){nodes{name,id,description}},comments{totalCount},issueFieldValues(first: 25){nodes{__typename,... on IssueFieldDateValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldNumberValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},valueNumber: value},... on IssueFieldSingleSelectValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldTextValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value}}}},pageInfo{hasNextPage,hasPreviousPage,startCursor,endCursor},totalCount},isPrivate}}"

	vars := map[string]any{
		"owner":            "octocat",
		"repo":             "hello",
		"states":           []any{"OPEN", "CLOSED"},
		"orderBy":          "CREATED_AT",
		"direction":        "DESC",
		"first":            float64(30),
		"after":            (*string)(nil),
		"issueFieldValues": []any{},
	}

	reqParams := map[string]any{"owner": "octocat", "repo": "hello"}

	t.Run("insiders mode disabled omits ifc label from result meta", func(t *testing.T) {
		matcher := githubv4mock.NewQueryMatcher(query, vars, makeResponse(false))
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))
		deps := BaseDeps{
			GQLClient: gqlClient,
		}
		handler := serverTool.Handler(deps)

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

		assert.Nil(t, result.Meta, "result meta should be nil when insiders mode is disabled")
	})

	t.Run("insiders mode enabled on public repo emits public untrusted label", func(t *testing.T) {
		matcher := githubv4mock.NewQueryMatcher(query, vars, makeResponse(false))
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))
		deps := BaseDeps{
			GQLClient:      gqlClient,
			featureChecker: featureCheckerFor(FeatureFlagIFCLabels),
		}
		handler := serverTool.Handler(deps)

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

		require.NotNil(t, result.Meta)
		ifcLabel, ok := result.Meta["ifc"]
		require.True(t, ok, "result meta should contain ifc key")

		ifcJSON, err := json.Marshal(ifcLabel)
		require.NoError(t, err)
		var ifcMap map[string]any
		require.NoError(t, json.Unmarshal(ifcJSON, &ifcMap))

		assert.Equal(t, "untrusted", ifcMap["integrity"])
		assert.Equal(t, "public", ifcMap["confidentiality"])
	})

	t.Run("insiders mode enabled on private repo emits private trusted label", func(t *testing.T) {
		matcher := githubv4mock.NewQueryMatcher(query, vars, makeResponse(true))
		gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(matcher))
		deps := BaseDeps{
			GQLClient:      gqlClient,
			featureChecker: featureCheckerFor(FeatureFlagIFCLabels),
		}
		handler := serverTool.Handler(deps)

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

		require.NotNil(t, result.Meta)
		ifcLabel, ok := result.Meta["ifc"]
		require.True(t, ok, "result meta should contain ifc key")

		ifcJSON, err := json.Marshal(ifcLabel)
		require.NoError(t, err)
		var ifcMap map[string]any
		require.NoError(t, json.Unmarshal(ifcJSON, &ifcMap))

		assert.Equal(t, "trusted", ifcMap["integrity"])
		assert.Equal(t, "private", ifcMap["confidentiality"])
	})
}

func Test_UpdateIssue(t *testing.T) {
	// Verify tool definition
	serverTool := IssueWrite(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "issue_write", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "method")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_number")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "title")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "body")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "labels")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "assignees")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "milestone")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "type")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "state")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "state_reason")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "duplicate_of")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_fields")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo"})

	// Mock issues for reuse across test cases
	mockBaseIssue := &github.Issue{
		Number:    github.Ptr(123),
		Title:     github.Ptr("Title"),
		Body:      github.Ptr("Description"),
		State:     github.Ptr("open"),
		HTMLURL:   github.Ptr("https://github.com/owner/repo/issues/123"),
		Assignees: []*github.User{{Login: github.Ptr("assignee1")}, {Login: github.Ptr("assignee2")}},
		Labels:    []*github.Label{{Name: github.Ptr("bug")}, {Name: github.Ptr("priority")}},
		Milestone: &github.Milestone{Number: github.Ptr(5)},
		Type:      &github.IssueType{Name: github.Ptr("Bug")},
	}

	mockUpdatedIssue := &github.Issue{
		Number:      github.Ptr(123),
		Title:       github.Ptr("Updated Title"),
		Body:        github.Ptr("Updated Description"),
		State:       github.Ptr("closed"),
		StateReason: github.Ptr("duplicate"),
		HTMLURL:     github.Ptr("https://github.com/owner/repo/issues/123"),
		Assignees:   []*github.User{{Login: github.Ptr("assignee1")}, {Login: github.Ptr("assignee2")}},
		Labels:      []*github.Label{{Name: github.Ptr("bug")}, {Name: github.Ptr("priority")}},
		Milestone:   &github.Milestone{Number: github.Ptr(5)},
		Type:        &github.IssueType{Name: github.Ptr("Bug")},
	}

	mockReopenedIssue := &github.Issue{
		Number:      github.Ptr(123),
		Title:       github.Ptr("Title"),
		State:       github.Ptr("open"),
		StateReason: github.Ptr("reopened"),
		HTMLURL:     github.Ptr("https://github.com/owner/repo/issues/123"),
	}

	// Mock GraphQL responses for reuse across test cases
	issueIDQueryResponse := githubv4mock.DataResponse(map[string]any{
		"repository": map[string]any{
			"issue": map[string]any{
				"id": "I_kwDOA0xdyM50BPaO",
			},
		},
	})

	duplicateIssueIDQueryResponse := githubv4mock.DataResponse(map[string]any{
		"repository": map[string]any{
			"issue": map[string]any{
				"id": "I_kwDOA0xdyM50BPaO",
			},
			"duplicateIssue": map[string]any{
				"id": "I_kwDOA0xdyM50BPbP",
			},
		},
	})

	closeSuccessResponse := githubv4mock.DataResponse(map[string]any{
		"closeIssue": map[string]any{
			"issue": map[string]any{
				"id":     "I_kwDOA0xdyM50BPaO",
				"number": 123,
				"url":    "https://github.com/owner/repo/issues/123",
				"state":  "CLOSED",
			},
		},
	})

	reopenSuccessResponse := githubv4mock.DataResponse(map[string]any{
		"reopenIssue": map[string]any{
			"issue": map[string]any{
				"id":     "I_kwDOA0xdyM50BPaO",
				"number": 123,
				"url":    "https://github.com/owner/repo/issues/123",
				"state":  "OPEN",
			},
		},
	})

	duplicateStateReason := IssueClosedStateReasonDuplicate

	tests := []struct {
		name             string
		mockedRESTClient *http.Client
		mockedGQLClient  *http.Client
		requestArgs      map[string]any
		expectError      bool
		expectedIssue    *github.Issue
		expectedErrMsg   string
	}{
		{
			name: "partial update of non-state fields only",
			mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
					"title": "Updated Title",
					"body":  "Updated Description",
				}).andThen(
					mockResponse(t, http.StatusOK, mockUpdatedIssue),
				),
			}),
			mockedGQLClient: githubv4mock.NewMockedHTTPClient(),
			requestArgs: map[string]any{
				"method":       "update",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(123),
				"title":        "Updated Title",
				"body":         "Updated Description",
			},
			expectError:   false,
			expectedIssue: mockUpdatedIssue,
		},
		{
			name: "partial update clears labels and assignees",
			mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
					"labels":    []any{},
					"assignees": []any{},
				}).andThen(
					mockResponse(t, http.StatusOK, &github.Issue{
						Number:  github.Ptr(123),
						HTMLURL: github.Ptr("https://github.com/owner/repo/issues/123"),
					}),
				),
			}),
			mockedGQLClient: githubv4mock.NewMockedHTTPClient(),
			requestArgs: map[string]any{
				"method":       "update",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(123),
				"labels":       []any{},
				"assignees":    []any{},
			},
			expectError: false,
			expectedIssue: &github.Issue{
				HTMLURL: github.Ptr("https://github.com/owner/repo/issues/123"),
			},
		},
		{
			name: "partial update with issue fields reconciled by names",
			mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
					"issue_field_values": []any{
						map[string]any{"field_id": float64(101), "value": "P1"},
						map[string]any{"field_id": float64(102), "value": "Acme"},
					},
					"title": "Updated Title",
				}).andThen(
					mockResponse(t, http.StatusOK, mockUpdatedIssue),
				),
			}),
			mockedGQLClient: githubv4mock.NewMockedHTTPClient(
				// fetch-and-merge: returns no existing fields so the incoming values are used as-is
				githubv4mock.NewQueryMatcher(
					"query($number:Int!$owner:String!$repo:String!){repository(owner: $owner, name: $repo){issue(number: $number){issueFieldValues(first: 25){nodes{__typename,... on IssueFieldDateValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldNumberValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},valueNumber: value},... on IssueFieldSingleSelectValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value},... on IssueFieldTextValue{field{... on IssueFieldDate{name,fullDatabaseId},... on IssueFieldNumber{name,fullDatabaseId},... on IssueFieldSingleSelect{name,fullDatabaseId},... on IssueFieldText{name,fullDatabaseId}},value}}}}}}",
					map[string]any{
						"owner":  githubv4.String("owner"),
						"repo":   githubv4.String("repo"),
						"number": githubv4.Int(123),
					},
					githubv4mock.DataResponse(map[string]any{
						"repository": map[string]any{
							"issue": map[string]any{
								"issueFieldValues": map[string]any{
									"nodes": []any{},
								},
							},
						},
					}),
				),
				githubv4mock.NewQueryMatcher(
					issueFieldWriteMetadataQuery{},
					map[string]any{
						"owner": githubv4.String("owner"),
						"repo":  githubv4.String("repo"),
					},
					githubv4mock.DataResponse(map[string]any{
						"repository": map[string]any{
							"issueFields": map[string]any{
								"nodes": []any{
									map[string]any{
										"__typename":     "IssueFieldSingleSelect",
										"fullDatabaseId": "101",
										"name":           "Priority",
										"dataType":       "single_select",
										"options":        []any{map[string]any{"fullDatabaseId": "9001", "name": "P1"}},
									},
									map[string]any{
										"__typename":     "IssueFieldText",
										"fullDatabaseId": "102",
										"name":           "Customer",
										"dataType":       "text",
									},
								},
							},
						},
					}),
				),
			),
			requestArgs: map[string]any{
				"method":       "update",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(123),
				"title":        "Updated Title",
				"issue_fields": []any{
					map[string]any{"field_name": "Priority", "field_option_name": "P1"},
					map[string]any{"field_name": "Customer", "value": "Acme"},
				},
			},
			expectError:   false,
			expectedIssue: mockUpdatedIssue,
		},
		{
			name: "issue not found when updating non-state fields only",
			mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusNotFound)
					_, _ = w.Write([]byte(`{"message": "Not Found"}`))
				}),
			}),
			mockedGQLClient: githubv4mock.NewMockedHTTPClient(),
			requestArgs: map[string]any{
				"method":       "update",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(999),
				"title":        "Updated Title",
			},
			expectError:    true,
			expectedErrMsg: "failed to update issue",
		},
		{
			name: "close issue as duplicate",
			mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockBaseIssue),
			}),
			mockedGQLClient: githubv4mock.NewMockedHTTPClient(
				githubv4mock.NewQueryMatcher(
					struct {
						Repository struct {
							Issue struct {
								ID githubv4.ID
							} `graphql:"issue(number: $issueNumber)"`
							DuplicateIssue struct {
								ID githubv4.ID
							} `graphql:"duplicateIssue: issue(number: $duplicateOf)"`
						} `graphql:"repository(owner: $owner, name: $repo)"`
					}{},
					map[string]any{
						"owner":       githubv4.String("owner"),
						"repo":        githubv4.String("repo"),
						"issueNumber": githubv4.Int(123),
						"duplicateOf": githubv4.Int(456),
					},
					duplicateIssueIDQueryResponse,
				),
				githubv4mock.NewMutationMatcher(
					struct {
						CloseIssue struct {
							Issue struct {
								ID     githubv4.ID
								Number githubv4.Int
								URL    githubv4.String
								State  githubv4.String
							}
						} `graphql:"closeIssue(input: $input)"`
					}{},
					CloseIssueInput{
						IssueID:          "I_kwDOA0xdyM50BPaO",
						StateReason:      &duplicateStateReason,
						DuplicateIssueID: githubv4.NewID("I_kwDOA0xdyM50BPbP"),
					},
					nil,
					closeSuccessResponse,
				),
			),
			requestArgs: map[string]any{
				"method":       "update",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(123),
				"state":        "closed",
				"state_reason": "duplicate",
				"duplicate_of": float64(456),
			},
			expectError:   false,
			expectedIssue: mockUpdatedIssue,
		},
		{
			name: "reopen issue",
			mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockBaseIssue),
			}),
			mockedGQLClient: githubv4mock.NewMockedHTTPClient(
				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(123),
					},
					issueIDQueryResponse,
				),
				githubv4mock.NewMutationMatcher(
					struct {
						ReopenIssue struct {
							Issue struct {
								ID     githubv4.ID
								Number githubv4.Int
								URL    githubv4.String
								State  githubv4.String
							}
						} `graphql:"reopenIssue(input: $input)"`
					}{},
					githubv4.ReopenIssueInput{
						IssueID: "I_kwDOA0xdyM50BPaO",
					},
					nil,
					reopenSuccessResponse,
				),
			),
			requestArgs: map[string]any{
				"method":       "update",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(123),
				"state":        "open",
			},
			expectError:   false,
			expectedIssue: mockReopenedIssue,
		},
		{
			name: "main issue not found when trying to close it",
			mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockBaseIssue),
			}),
			mockedGQLClient: githubv4mock.NewMockedHTTPClient(
				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(999),
					},
					githubv4mock.ErrorResponse("Could not resolve to an Issue with the number of 999."),
				),
			),
			requestArgs: map[string]any{
				"method":       "update",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(999),
				"state":        "closed",
				"state_reason": "not_planned",
			},
			expectError:    true,
			expectedErrMsg: "Failed to find issues",
		},
		{
			name: "duplicate issue not found when closing as duplicate",
			mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockBaseIssue),
			}),
			mockedGQLClient: githubv4mock.NewMockedHTTPClient(
				githubv4mock.NewQueryMatcher(
					struct {
						Repository struct {
							Issue struct {
								ID githubv4.ID
							} `graphql:"issue(number: $issueNumber)"`
							DuplicateIssue struct {
								ID githubv4.ID
							} `graphql:"duplicateIssue: issue(number: $duplicateOf)"`
						} `graphql:"repository(owner: $owner, name: $repo)"`
					}{},
					map[string]any{
						"owner":       githubv4.String("owner"),
						"repo":        githubv4.String("repo"),
						"issueNumber": githubv4.Int(123),
						"duplicateOf": githubv4.Int(999),
					},
					githubv4mock.ErrorResponse("Could not resolve to an Issue with the number of 999."),
				),
			),
			requestArgs: map[string]any{
				"method":       "update",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(123),
				"state":        "closed",
				"state_reason": "duplicate",
				"duplicate_of": float64(999),
			},
			expectError:    true,
			expectedErrMsg: "Failed to find issues",
		},
		{
			name: "close as duplicate with combined non-state updates",
			mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
					"title":     "Updated Title",
					"body":      "Updated Description",
					"labels":    []any{"bug", "priority"},
					"assignees": []any{"assignee1", "assignee2"},
					"milestone": float64(5),
					"type":      "Bug",
				}).andThen(
					mockResponse(t, http.StatusOK, &github.Issue{
						Number:    github.Ptr(123),
						Title:     github.Ptr("Updated Title"),
						Body:      github.Ptr("Updated Description"),
						Labels:    []*github.Label{{Name: github.Ptr("bug")}, {Name: github.Ptr("priority")}},
						Assignees: []*github.User{{Login: github.Ptr("assignee1")}, {Login: github.Ptr("assignee2")}},
						Milestone: &github.Milestone{Number: github.Ptr(5)},
						Type:      &github.IssueType{Name: github.Ptr("Bug")},
						State:     github.Ptr("open"), // Still open after REST update
						HTMLURL:   github.Ptr("https://github.com/owner/repo/issues/123"),
					}),
				),
			}),
			mockedGQLClient: githubv4mock.NewMockedHTTPClient(
				githubv4mock.NewQueryMatcher(
					struct {
						Repository struct {
							Issue struct {
								ID githubv4.ID
							} `graphql:"issue(number: $issueNumber)"`
							DuplicateIssue struct {
								ID githubv4.ID
							} `graphql:"duplicateIssue: issue(number: $duplicateOf)"`
						} `graphql:"repository(owner: $owner, name: $repo)"`
					}{},
					map[string]any{
						"owner":       githubv4.String("owner"),
						"repo":        githubv4.String("repo"),
						"issueNumber": githubv4.Int(123),
						"duplicateOf": githubv4.Int(456),
					},
					duplicateIssueIDQueryResponse,
				),
				githubv4mock.NewMutationMatcher(
					struct {
						CloseIssue struct {
							Issue struct {
								ID     githubv4.ID
								Number githubv4.Int
								URL    githubv4.String
								State  githubv4.String
							}
						} `graphql:"closeIssue(input: $input)"`
					}{},
					CloseIssueInput{
						IssueID:          "I_kwDOA0xdyM50BPaO",
						StateReason:      &duplicateStateReason,
						DuplicateIssueID: githubv4.NewID("I_kwDOA0xdyM50BPbP"),
					},
					nil,
					closeSuccessResponse,
				),
			),
			requestArgs: map[string]any{
				"method":       "update",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(123),
				"title":        "Updated Title",
				"body":         "Updated Description",
				"labels":       []any{"bug", "priority"},
				"assignees":    []any{"assignee1", "assignee2"},
				"milestone":    float64(5),
				"type":         "Bug",
				"state":        "closed",
				"state_reason": "duplicate",
				"duplicate_of": float64(456),
			},
			expectError:   false,
			expectedIssue: mockUpdatedIssue,
		},
		{
			name:             "duplicate_of without duplicate state_reason should fail",
			mockedRESTClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			mockedGQLClient:  githubv4mock.NewMockedHTTPClient(),
			requestArgs: map[string]any{
				"method":       "update",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(123),
				"state":        "closed",
				"state_reason": "completed",
				"duplicate_of": float64(456),
			},
			expectError:    true,
			expectedErrMsg: "duplicate_of can only be used when state_reason is 'duplicate'",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Setup clients with mocks
			restClient := mustNewGHClient(t, tc.mockedRESTClient)
			gqlClient := githubv4.NewClient(tc.mockedGQLClient)
			deps := BaseDeps{
				Client:    restClient,
				GQLClient: gqlClient,
			}
			handler := serverTool.Handler(deps)

			// Create call request
			request := createMCPRequest(tc.requestArgs)

			// Call handler
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)

			// Verify results
			if tc.expectError || tc.expectedErrMsg != "" {
				require.NoError(t, err)
				require.True(t, result.IsError)
				errorContent := getErrorResult(t, result)
				if tc.expectedErrMsg != "" {
					assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
				}
				return
			}

			require.NoError(t, err)
			if result.IsError {
				t.Fatalf("Unexpected error result: %s", getErrorResult(t, result).Text)
			}

			require.False(t, result.IsError)

			// Parse the result and get the text content
			textContent := getTextResult(t, result)

			// Unmarshal and verify the minimal result
			var updateResp MinimalResponse
			err = json.Unmarshal([]byte(textContent.Text), &updateResp)
			require.NoError(t, err)

			assert.Equal(t, tc.expectedIssue.GetHTMLURL(), updateResp.URL)
		})
	}
}

func Test_UpdateIssueClearsLabelsAndAssignees(t *testing.T) {
	serverTool := IssueWrite(translations.NullTranslationHelper)
	updatedIssue := &github.Issue{
		Number:  github.Ptr(8),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/8"),
	}

	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, map[string]any{
			"labels":    []any{},
			"assignees": []any{},
		}).andThen(mockResponse(t, http.StatusOK, updatedIssue)),
	}))
	gqlClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient())
	deps := BaseDeps{
		Client:    client,
		GQLClient: gqlClient,
	}
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"method":       "update",
		"owner":        "owner",
		"repo":         "repo",
		"issue_number": float64(8),
		"labels":       []any{},
		"assignees":    []any{},
	})
	result, err := handler(ContextWithDeps(context.Background(), deps), &request)

	require.NoError(t, err)
	if result.IsError {
		t.Fatalf("Unexpected error result: %s", getErrorResult(t, result).Text)
	}
	textContent := getTextResult(t, result)

	var updateResp MinimalResponse
	require.NoError(t, json.Unmarshal([]byte(textContent.Text), &updateResp))
	assert.Equal(t, updatedIssue.GetHTMLURL(), updateResp.URL)
}

func Test_ParseISOTimestamp(t *testing.T) {
	tests := []struct {
		name         string
		input        string
		expectedErr  bool
		expectedTime time.Time
	}{
		{
			name:         "valid RFC3339 format",
			input:        "2023-01-15T14:30:00Z",
			expectedErr:  false,
			expectedTime: time.Date(2023, 1, 15, 14, 30, 0, 0, time.UTC),
		},
		{
			name:         "valid date only format",
			input:        "2023-01-15",
			expectedErr:  false,
			expectedTime: time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC),
		},
		{
			name:        "empty timestamp",
			input:       "",
			expectedErr: true,
		},
		{
			name:        "invalid format",
			input:       "15/01/2023",
			expectedErr: true,
		},
		{
			name:        "invalid date",
			input:       "2023-13-45",
			expectedErr: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			parsedTime, err := parseISOTimestamp(tc.input)

			if tc.expectedErr {
				assert.Error(t, err)
			} else {
				assert.NoError(t, err)
				assert.Equal(t, tc.expectedTime, parsedTime)
			}
		})
	}
}

func Test_GetIssueComments(t *testing.T) {
	// Verify tool definition once
	serverTool := IssueRead(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "issue_read", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "method")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_number")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "page")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "perPage")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo", "issue_number"})

	// Setup mock comments for success case
	mockComments := []*github.IssueComment{
		{
			ID:   github.Ptr(int64(123)),
			Body: github.Ptr("This is the first comment"),
			User: &github.User{
				Login: github.Ptr("user1"),
			},
			CreatedAt: &github.Timestamp{Time: time.Now().Add(-time.Hour * 24)},
		},
		{
			ID:   github.Ptr(int64(456)),
			Body: github.Ptr("This is the second comment"),
			User: &github.User{
				Login: github.Ptr("user2"),
			},
			CreatedAt: &github.Timestamp{Time: time.Now().Add(-time.Hour)},
		},
	}

	tests := []struct {
		name             string
		mockedClient     *http.Client
		requestArgs      map[string]any
		expectError      bool
		expectedComments []*github.IssueComment
		expectedErrMsg   string
		lockdownEnabled  bool
	}{
		{
			name: "successful comments retrieval",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesCommentsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockComments),
			}),
			requestArgs: map[string]any{
				"method":       "get_comments",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectError:      false,
			expectedComments: mockComments,
		},
		{
			name: "successful comments retrieval with pagination",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesCommentsByOwnerByRepoByIssueNumber: expectQueryParams(t, map[string]string{
					"page":     "2",
					"per_page": "10",
				}).andThen(
					mockResponse(t, http.StatusOK, mockComments),
				),
			}),
			requestArgs: map[string]any{
				"method":       "get_comments",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"page":         float64(2),
				"perPage":      float64(10),
			},
			expectError:      false,
			expectedComments: mockComments,
		},
		{
			name: "issue not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesCommentsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message": "Issue not found"}`),
			}),
			requestArgs: map[string]any{
				"method":       "get_comments",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(999),
			},
			expectError:    true,
			expectedErrMsg: "failed to get issue comments",
		},
		{
			name: "lockdown enabled filters comments without push access",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesCommentsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, []*github.IssueComment{
					{
						ID:   github.Ptr(int64(789)),
						Body: github.Ptr("Maintainer comment"),
						User: &github.User{Login: github.Ptr("maintainer")},
					},
					{
						ID:   github.Ptr(int64(790)),
						Body: github.Ptr("External user comment"),
						User: &github.User{Login: github.Ptr("testuser")},
					},
				}),
			}),
			requestArgs: map[string]any{
				"method":       "get_comments",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectError: false,
			expectedComments: []*github.IssueComment{
				{
					ID:   github.Ptr(int64(789)),
					Body: github.Ptr("Maintainer comment"),
					User: &github.User{Login: github.Ptr("maintainer")},
				},
			},
			lockdownEnabled: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Setup client with mock
			client := mustNewGHClient(t, tc.mockedClient)
			var restClient *github.Client
			if tc.lockdownEnabled {
				restClient = mockRESTPermissionServer(t, "read", map[string]string{
					"maintainer": "write",
					"testuser":   "read",
				})
			}
			cache := stubRepoAccessCache(restClient, 15*time.Minute)
			flags := stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled})
			deps := BaseDeps{
				Client:          client,
				GQLClient:       defaultGQLClient,
				RepoAccessCache: cache,
				Flags:           flags,
			}
			handler := serverTool.Handler(deps)

			// Create call request
			request := createMCPRequest(tc.requestArgs)

			// Call handler
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)

			// Verify results
			if tc.expectError {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.expectedErrMsg)
				return
			}

			require.NoError(t, err)
			textContent := getTextResult(t, result)

			// Unmarshal and verify the result
			var returnedComments []MinimalIssueComment
			err = json.Unmarshal([]byte(textContent.Text), &returnedComments)
			require.NoError(t, err)
			assert.Equal(t, len(tc.expectedComments), len(returnedComments))
			for i := range tc.expectedComments {
				require.NotNil(t, tc.expectedComments[i].User)
				require.NotNil(t, returnedComments[i].User)
				assert.Equal(t, tc.expectedComments[i].GetID(), returnedComments[i].ID)
				assert.Equal(t, tc.expectedComments[i].GetBody(), returnedComments[i].Body)
				assert.Equal(t, tc.expectedComments[i].GetUser().GetLogin(), returnedComments[i].User.Login)
			}
		})
	}
}

func Test_GetIssueLabels(t *testing.T) {
	t.Parallel()

	// Verify tool definition
	serverTool := IssueRead(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "issue_read", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "method")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_number")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo", "issue_number"})

	tests := []struct {
		name               string
		requestArgs        map[string]any
		mockedClient       *http.Client
		expectToolError    bool
		expectedToolErrMsg string
	}{
		{
			name: "successful issue labels listing",
			requestArgs: map[string]any{
				"method":       "get_labels",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(123),
			},
			mockedClient: githubv4mock.NewMockedHTTPClient(
				githubv4mock.NewQueryMatcher(
					struct {
						Repository struct {
							Issue struct {
								Labels struct {
									Nodes []struct {
										ID          githubv4.ID
										Name        githubv4.String
										Color       githubv4.String
										Description githubv4.String
									}
									TotalCount githubv4.Int
								} `graphql:"labels(first: 100)"`
							} `graphql:"issue(number: $issueNumber)"`
						} `graphql:"repository(owner: $owner, name: $repo)"`
					}{},
					map[string]any{
						"owner":       githubv4.String("owner"),
						"repo":        githubv4.String("repo"),
						"issueNumber": githubv4.Int(123),
					},
					githubv4mock.DataResponse(map[string]any{
						"repository": map[string]any{
							"issue": map[string]any{
								"labels": map[string]any{
									"nodes": []any{
										map[string]any{
											"id":          githubv4.ID("label-1"),
											"name":        githubv4.String("bug"),
											"color":       githubv4.String("d73a4a"),
											"description": githubv4.String("Something isn't working"),
										},
									},
									"totalCount": githubv4.Int(1),
								},
							},
						},
					}),
				),
			),
			expectToolError: false,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			gqlClient := githubv4.NewClient(tc.mockedClient)
			client := mustNewGHClient(t, nil)
			deps := BaseDeps{
				Client:          client,
				GQLClient:       gqlClient,
				RepoAccessCache: stubRepoAccessCache(nil, 15*time.Minute),
				Flags:           stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
			}
			handler := serverTool.Handler(deps)

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

			require.NoError(t, err)
			assert.NotNil(t, result)

			if tc.expectToolError {
				assert.True(t, result.IsError)
				if tc.expectedToolErrMsg != "" {
					textContent := getErrorResult(t, result)
					assert.Contains(t, textContent.Text, tc.expectedToolErrMsg)
				}
			} else {
				assert.False(t, result.IsError)
			}
		})
	}
}

func Test_GetIssueParent(t *testing.T) {
	t.Parallel()

	serverTool := IssueRead(translations.NullTranslationHelper)

	parentMatcherStruct := struct {
		Repository struct {
			Issue struct {
				Parent *struct {
					Number githubv4.Int
					Title  githubv4.String
					State  githubv4.String
					URL    githubv4.String
					Author struct {
						Login githubv4.String
					}
					Repository struct {
						NameWithOwner githubv4.String
					}
				}
			} `graphql:"issue(number: $issueNumber)"`
		} `graphql:"repository(owner: $owner, name: $repo)"`
	}{}

	vars := map[string]any{
		"owner":       githubv4.String("owner"),
		"repo":        githubv4.String("repo"),
		"issueNumber": githubv4.Int(123),
	}

	parentResponse := func(authorLogin string) githubv4mock.GQLResponse {
		return githubv4mock.DataResponse(map[string]any{
			"repository": map[string]any{
				"issue": map[string]any{
					"parent": map[string]any{
						"number": githubv4.Int(42),
						"title":  githubv4.String("Parent\u202e issue"),
						"state":  githubv4.String("OPEN"),
						"url":    githubv4.String("https://github.com/owner/repo/issues/42"),
						"author": map[string]any{
							"login": githubv4.String(authorLogin),
						},
						"repository": map[string]any{
							"nameWithOwner": githubv4.String("owner/repo"),
						},
					},
				},
			},
		})
	}

	tests := []struct {
		name            string
		mockedClient    *http.Client
		lockdownEnabled bool
		restOverrides   map[string]string
		expectToolError bool
		expectedText    string
	}{
		{
			name: "issue has a parent",
			mockedClient: githubv4mock.NewMockedHTTPClient(
				githubv4mock.NewQueryMatcher(
					parentMatcherStruct,
					vars,
					parentResponse("author"),
				),
			),
			expectedText: `"title":"Parent issue"`,
		},
		{
			name: "issue has no parent",
			mockedClient: githubv4mock.NewMockedHTTPClient(
				githubv4mock.NewQueryMatcher(
					parentMatcherStruct,
					vars,
					githubv4mock.DataResponse(map[string]any{
						"repository": map[string]any{
							"issue": map[string]any{
								"parent": nil,
							},
						},
					}),
				),
			),
			expectedText: `"parent":null`,
		},
		{
			name: "graphql error",
			mockedClient: githubv4mock.NewMockedHTTPClient(
				githubv4mock.NewQueryMatcher(
					parentMatcherStruct,
					vars,
					githubv4mock.ErrorResponse("issue not found"),
				),
			),
			expectToolError: true,
		},
		{
			name: "lockdown enabled - parent author has push access",
			mockedClient: githubv4mock.NewMockedHTTPClient(
				githubv4mock.NewQueryMatcher(
					parentMatcherStruct,
					vars,
					parentResponse("maintainer"),
				),
			),
			lockdownEnabled: true,
			restOverrides:   map[string]string{"maintainer": "write"},
			expectedText:    `"title":"Parent issue"`,
		},
		{
			name: "lockdown enabled - parent author lacks push access",
			mockedClient: githubv4mock.NewMockedHTTPClient(
				githubv4mock.NewQueryMatcher(
					parentMatcherStruct,
					vars,
					parentResponse("externaluser"),
				),
			),
			lockdownEnabled: true,
			restOverrides:   map[string]string{"externaluser": "read"},
			expectedText:    `"parent":null`,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			gqlClient := githubv4.NewClient(tc.mockedClient)
			client := mustNewGHClient(t, nil)

			var restClient *github.Client
			if tc.lockdownEnabled {
				restClient = mockRESTPermissionServer(t, "read", tc.restOverrides)
			}
			deps := BaseDeps{
				Client:          client,
				GQLClient:       gqlClient,
				RepoAccessCache: stubRepoAccessCache(restClient, 15*time.Minute),
				Flags:           stubFeatureFlags(map[string]bool{"lockdown-mode": tc.lockdownEnabled}),
			}
			handler := serverTool.Handler(deps)

			request := createMCPRequest(map[string]any{
				"method":       "get_parent",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(123),
			})
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)

			require.NoError(t, err)
			require.NotNil(t, result)

			if tc.expectToolError {
				assert.True(t, result.IsError)
				return
			}
			assert.False(t, result.IsError)
			textContent := getTextResult(t, result)
			assert.Contains(t, textContent.Text, tc.expectedText)
		})
	}
}

func Test_AddSubIssue(t *testing.T) {
	// Verify tool definition once
	serverTool := SubIssueWrite(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "sub_issue_write", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "method")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_number")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "sub_issue_id")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "replace_parent")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo", "issue_number", "sub_issue_id"})

	// Setup mock issue for success case (matches GitHub API response format)
	mockIssue := &github.Issue{
		Number:  github.Ptr(42),
		Title:   github.Ptr("Parent Issue"),
		Body:    github.Ptr("This is the parent issue with a sub-issue"),
		State:   github.Ptr("open"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"),
		User: &github.User{
			Login: github.Ptr("testuser"),
		},
		Labels: []*github.Label{
			{
				Name:        github.Ptr("enhancement"),
				Color:       github.Ptr("84b6eb"),
				Description: github.Ptr("New feature or request"),
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedIssue  *github.Issue
		expectedErrMsg string
	}{
		{
			name: "successful sub-issue addition with all parameters",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusCreated, mockIssue),
			}),
			requestArgs: map[string]any{
				"method":         "add",
				"owner":          "owner",
				"repo":           "repo",
				"issue_number":   float64(42),
				"sub_issue_id":   float64(123),
				"replace_parent": true,
			},
			expectError:   false,
			expectedIssue: mockIssue,
		},
		{
			name: "successful sub-issue addition with minimal parameters",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusCreated, mockIssue),
			}),
			requestArgs: map[string]any{
				"method":       "add",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(456),
			},
			expectError:   false,
			expectedIssue: mockIssue,
		},
		{
			name: "successful sub-issue addition with replace_parent false",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusCreated, mockIssue),
			}),
			requestArgs: map[string]any{
				"method":         "add",
				"owner":          "owner",
				"repo":           "repo",
				"issue_number":   float64(42),
				"sub_issue_id":   float64(789),
				"replace_parent": false,
			},
			expectError:   false,
			expectedIssue: mockIssue,
		},
		{
			name: "parent issue not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message": "Parent issue not found"}`),
			}),
			requestArgs: map[string]any{
				"method":       "add",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(999),
				"sub_issue_id": float64(123),
			},
			expectError:    false,
			expectedErrMsg: "failed to add sub-issue",
		},
		{
			name: "sub-issue not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message": "Sub-issue not found"}`),
			}),
			requestArgs: map[string]any{
				"method":       "add",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(999),
			},
			expectError:    false,
			expectedErrMsg: "failed to add sub-issue",
		},
		{
			name: "validation failed - sub-issue cannot be parent of itself",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusUnprocessableEntity, `{"message": "Validation failed", "errors": [{"message": "Sub-issue cannot be a parent of itself"}]}`),
			}),
			requestArgs: map[string]any{
				"method":       "add",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(42),
			},
			expectError:    false,
			expectedErrMsg: "failed to add sub-issue",
		},
		{
			name: "insufficient permissions",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusForbidden, `{"message": "Must have write access to repository"}`),
			}),
			requestArgs: map[string]any{
				"method":       "add",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
			},
			expectError:    false,
			expectedErrMsg: "failed to add sub-issue",
		},
		{
			name:         "missing required parameter owner",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"method":       "add",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
			},
			expectError:    false,
			expectedErrMsg: "missing required parameter: owner",
		},
		{
			name:         "missing required parameter sub_issue_id",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"method":       "add",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectError:    false,
			expectedErrMsg: "missing required parameter: sub_issue_id",
		},
	}

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

			// Create call request
			request := createMCPRequest(tc.requestArgs)

			// Call handler
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)

			// Verify results
			if tc.expectError {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.expectedErrMsg)
				return
			}

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

			require.NoError(t, err)

			// Parse the result and get the text content if no error
			textContent := getTextResult(t, result)

			// Unmarshal and verify the result
			var returnedIssue github.Issue
			err = json.Unmarshal([]byte(textContent.Text), &returnedIssue)
			require.NoError(t, err)
			assert.Equal(t, *tc.expectedIssue.Number, *returnedIssue.Number)
			assert.Equal(t, *tc.expectedIssue.Title, *returnedIssue.Title)
			assert.Equal(t, *tc.expectedIssue.Body, *returnedIssue.Body)
			assert.Equal(t, *tc.expectedIssue.State, *returnedIssue.State)
			assert.Equal(t, *tc.expectedIssue.HTMLURL, *returnedIssue.HTMLURL)
			assert.Equal(t, *tc.expectedIssue.User.Login, *returnedIssue.User.Login)
		})
	}
}

func Test_GetSubIssues(t *testing.T) {
	// Verify tool definition once
	serverTool := IssueRead(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "issue_read", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "method")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_number")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "page")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "perPage")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo", "issue_number"})

	// Setup mock sub-issues for success case
	mockSubIssues := []*github.Issue{
		{
			Number:  github.Ptr(123),
			Title:   github.Ptr("Sub-issue 1"),
			Body:    github.Ptr("This is the first sub-issue"),
			State:   github.Ptr("open"),
			HTMLURL: github.Ptr("https://github.com/owner/repo/issues/123"),
			User: &github.User{
				Login: github.Ptr("user1"),
			},
			Labels: []*github.Label{
				{
					Name:        github.Ptr("bug"),
					Color:       github.Ptr("d73a4a"),
					Description: github.Ptr("Something isn't working"),
				},
			},
		},
		{
			Number:  github.Ptr(124),
			Title:   github.Ptr("Sub-issue 2"),
			Body:    github.Ptr("This is the second sub-issue"),
			State:   github.Ptr("closed"),
			HTMLURL: github.Ptr("https://github.com/owner/repo/issues/124"),
			User: &github.User{
				Login: github.Ptr("user2"),
			},
			Assignees: []*github.User{
				{Login: github.Ptr("assignee1")},
			},
		},
	}

	tests := []struct {
		name              string
		mockedClient      *http.Client
		requestArgs       map[string]any
		expectError       bool
		expectedSubIssues []*github.Issue
		expectedErrMsg    string
	}{
		{
			name: "successful sub-issues listing with minimal parameters",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockSubIssues),
			}),
			requestArgs: map[string]any{
				"method":       "get_sub_issues",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectError:       false,
			expectedSubIssues: mockSubIssues,
		},
		{
			name: "successful sub-issues listing with pagination",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber: expectQueryParams(t, map[string]string{
					"page":     "2",
					"per_page": "10",
				}).andThen(
					mockResponse(t, http.StatusOK, mockSubIssues),
				),
			}),
			requestArgs: map[string]any{
				"method":       "get_sub_issues",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"page":         float64(2),
				"perPage":      float64(10),
			},
			expectError:       false,
			expectedSubIssues: mockSubIssues,
		},
		{
			name: "successful sub-issues listing with empty result",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, []*github.Issue{}),
			}),
			requestArgs: map[string]any{
				"method":       "get_sub_issues",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectError:       false,
			expectedSubIssues: []*github.Issue{},
		},
		{
			name: "parent issue not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
			}),
			requestArgs: map[string]any{
				"method":       "get_sub_issues",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(999),
			},
			expectError:    false,
			expectedErrMsg: "failed to list sub-issues",
		},
		{
			name: "repository not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
			}),
			requestArgs: map[string]any{
				"method":       "get_sub_issues",
				"owner":        "nonexistent",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectError:    false,
			expectedErrMsg: "failed to list sub-issues",
		},
		{
			name: "sub-issues feature gone/deprecated",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesSubIssuesByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusGone, `{"message": "This feature has been deprecated"}`),
			}),
			requestArgs: map[string]any{
				"method":       "get_sub_issues",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectError:    false,
			expectedErrMsg: "failed to list sub-issues",
		},
		{
			name:         "missing required parameter owner",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"method":       "get_sub_issues",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectError:    false,
			expectedErrMsg: "missing required parameter: owner",
		},
		{
			name:         "missing required parameter issue_number",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"method": "get_sub_issues",
				"owner":  "owner",
				"repo":   "repo",
			},
			expectError:    false,
			expectedErrMsg: "missing required parameter: issue_number",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			// Setup client with mock
			client := mustNewGHClient(t, tc.mockedClient)
			gqlClient := githubv4.NewClient(nil)
			deps := BaseDeps{
				Client:          client,
				GQLClient:       gqlClient,
				RepoAccessCache: stubRepoAccessCache(nil, 15*time.Minute),
				Flags:           stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
			}
			handler := serverTool.Handler(deps)

			// Create call request
			request := createMCPRequest(tc.requestArgs)

			// Call handler
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)

			// Verify results
			if tc.expectError {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.expectedErrMsg)
				return
			}

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

			require.NoError(t, err)

			// Parse the result and get the text content if no error
			textContent := getTextResult(t, result)

			// Unmarshal and verify the result
			var returnedSubIssues []*github.Issue
			err = json.Unmarshal([]byte(textContent.Text), &returnedSubIssues)
			require.NoError(t, err)

			assert.Len(t, returnedSubIssues, len(tc.expectedSubIssues))
			for i, subIssue := range returnedSubIssues {
				if i < len(tc.expectedSubIssues) {
					assert.Equal(t, *tc.expectedSubIssues[i].Number, *subIssue.Number)
					assert.Equal(t, *tc.expectedSubIssues[i].Title, *subIssue.Title)
					assert.Equal(t, *tc.expectedSubIssues[i].State, *subIssue.State)
					assert.Equal(t, *tc.expectedSubIssues[i].HTMLURL, *subIssue.HTMLURL)
					assert.Equal(t, *tc.expectedSubIssues[i].User.Login, *subIssue.User.Login)

					if tc.expectedSubIssues[i].Body != nil {
						assert.Equal(t, *tc.expectedSubIssues[i].Body, *subIssue.Body)
					}
				}

			}
		})
	}
}

func TestAddIssueComment(t *testing.T) {
	t.Parallel()

	serverTool := AddIssueComment(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "add_issue_comment", tool.Name)
	assert.NotEmpty(t, tool.Description)
	schema := tool.InputSchema.(*jsonschema.Schema)
	assert.Contains(t, schema.Properties, "owner")
	assert.Contains(t, schema.Properties, "repo")
	assert.Contains(t, schema.Properties, "issue_number")
	assert.Contains(t, schema.Properties, "comment_id")
	assert.Contains(t, schema.Properties, "body")
	assert.Contains(t, schema.Properties, "reaction")
	assert.ElementsMatch(t, schema.Required, []string{"owner", "repo", "issue_number"})

	mockComment := &github.IssueComment{
		ID:      github.Ptr(int64(456)),
		Body:    github.Ptr("This is a comment"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42#issuecomment-456"),
	}
	mockReaction := &github.Reaction{
		ID:      github.Ptr(int64(789)),
		Content: github.Ptr("heart"),
	}
	mockIssueComment := &github.IssueComment{
		ID:       github.Ptr(int64(999)),
		IssueURL: github.Ptr("https://api.github.com/repos/owner/repo/issues/42"),
	}
	commentCreatedAfterReactionFailure := &atomic.Bool{}

	tests := []struct {
		name               string
		mockedClient       *http.Client
		requestArgs        map[string]any
		expectToolError    bool
		expectedToolErrMsg string
		unexpectedCall     *atomic.Bool
	}{
		{
			name: "successful comment on issue",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesCommentsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusCreated, mockComment),
			}),
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"body":         "This is a comment",
			},
		},
		{
			name: "successful reaction to issue",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesReactionsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusCreated, mockReaction),
			}),
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"reaction":     "heart",
			},
		},
		{
			name: "successful reaction to issue comment",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesCommentByOwnerByRepoByCommentID:            mockResponse(t, http.StatusOK, mockIssueComment),
				PostReposIssuesCommentsReactionsByOwnerByRepoByCommentID: mockResponse(t, http.StatusCreated, mockReaction),
			}),
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"comment_id":   float64(999),
				"reaction":     "heart",
			},
		},
		{
			name: "issue comment reaction requires matching issue_number",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesCommentByOwnerByRepoByCommentID: mockResponse(t, http.StatusOK, &github.IssueComment{
					ID:       github.Ptr(int64(999)),
					IssueURL: github.Ptr("https://api.github.com/repos/owner/repo/issues/43"),
				}),
			}),
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"comment_id":   float64(999),
				"reaction":     "heart",
			},
			expectToolError:    true,
			expectedToolErrMsg: "comment_id does not belong to issue_number 42",
		},
		{
			name: "issue comment lookup fails",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposIssuesCommentByOwnerByRepoByCommentID: func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusNotFound)
					_, _ = w.Write([]byte(`{"message": "Not Found"}`))
				},
			}),
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"comment_id":   float64(999),
				"reaction":     "heart",
			},
			expectToolError:    true,
			expectedToolErrMsg: "failed to get issue comment",
		},
		{
			name: "successful comment and reaction to issue",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesCommentsByOwnerByRepoByIssueNumber:  mockResponse(t, http.StatusCreated, mockComment),
				PostReposIssuesReactionsByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusCreated, mockReaction),
			}),
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"body":         "This is a comment",
				"reaction":     "heart",
			},
		},
		{
			name: "missing body and reaction",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectToolError:    true,
			expectedToolErrMsg: "at least one of body or reaction is required",
		},
		{
			name: "missing issue_number for reaction",
			requestArgs: map[string]any{
				"owner":    "owner",
				"repo":     "repo",
				"reaction": "heart",
			},
			expectToolError:    true,
			expectedToolErrMsg: "missing required parameter: issue_number",
		},
		{
			name: "missing issue_number for body",
			requestArgs: map[string]any{
				"owner": "owner",
				"repo":  "repo",
				"body":  "This is a comment",
			},
			expectToolError:    true,
			expectedToolErrMsg: "missing required parameter: issue_number",
		},
		{
			name: "comment_id without reaction",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"comment_id":   float64(999),
			},
			expectToolError:    true,
			expectedToolErrMsg: "comment_id can only be provided when reaction is provided",
		},
		{
			name: "negative comment_id",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"comment_id":   float64(-1),
				"reaction":     "heart",
			},
			expectToolError:    true,
			expectedToolErrMsg: "comment_id must be greater than 0",
		},
		{
			name: "comment_id with body",
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"comment_id":   float64(999),
				"body":         "This is a comment",
				"reaction":     "heart",
			},
			expectToolError:    true,
			expectedToolErrMsg: "comment_id cannot be combined with body",
		},
		{
			name: "does not create comment when reaction fails",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostReposIssuesReactionsByOwnerByRepoByIssueNumber: func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusInternalServerError)
					_, _ = w.Write([]byte(`{"message": "server error"}`))
				},
				PostReposIssuesCommentsByOwnerByRepoByIssueNumber: func(w http.ResponseWriter, _ *http.Request) {
					commentCreatedAfterReactionFailure.Store(true)
					w.WriteHeader(http.StatusCreated)
					responseData, _ := json.Marshal(mockComment)
					_, _ = w.Write(responseData)
				},
			}),
			requestArgs: map[string]any{
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"body":         "This is a comment",
				"reaction":     "heart",
			},
			expectToolError:    true,
			expectedToolErrMsg: "failed to add reaction to issue",
			unexpectedCall:     commentCreatedAfterReactionFailure,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			client := mustNewGHClient(t, tc.mockedClient)
			deps := BaseDeps{Client: client}
			handler := serverTool.Handler(deps)

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

			if tc.expectToolError {
				require.True(t, result.IsError)
				errorContent := getErrorResult(t, result)
				assert.Contains(t, errorContent.Text, tc.expectedToolErrMsg)
				if tc.unexpectedCall != nil {
					assert.False(t, tc.unexpectedCall.Load())
				}
				return
			}

			require.False(t, result.IsError)
			textContent := getTextResult(t, result)
			if _, ok := tc.requestArgs["body"]; ok {
				assert.Contains(t, textContent.Text, "456")
			}
			if _, ok := tc.requestArgs["reaction"]; ok {
				assert.Contains(t, textContent.Text, "789")
			}
		})
	}
}

func Test_RemoveSubIssue(t *testing.T) {
	// Verify tool definition once
	serverTool := SubIssueWrite(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "sub_issue_write", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "method")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_number")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "sub_issue_id")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo", "issue_number", "sub_issue_id"})

	// Setup mock issue for success case (matches GitHub API response format - the updated parent issue)
	mockIssue := &github.Issue{
		Number:  github.Ptr(42),
		Title:   github.Ptr("Parent Issue"),
		Body:    github.Ptr("This is the parent issue after sub-issue removal"),
		State:   github.Ptr("open"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"),
		User: &github.User{
			Login: github.Ptr("testuser"),
		},
		Labels: []*github.Label{
			{
				Name:        github.Ptr("enhancement"),
				Color:       github.Ptr("84b6eb"),
				Description: github.Ptr("New feature or request"),
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedIssue  *github.Issue
		expectedErrMsg string
	}{
		{
			name: "successful sub-issue removal",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue),
			}),
			requestArgs: map[string]any{
				"method":       "remove",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
			},
			expectError:   false,
			expectedIssue: mockIssue,
		},
		{
			name: "parent issue not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
			}),
			requestArgs: map[string]any{
				"method":       "remove",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(999),
				"sub_issue_id": float64(123),
			},
			expectError:    false,
			expectedErrMsg: "failed to remove sub-issue",
		},
		{
			name: "sub-issue not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message": "Sub-issue not found"}`),
			}),
			requestArgs: map[string]any{
				"method":       "remove",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(999),
			},
			expectError:    false,
			expectedErrMsg: "failed to remove sub-issue",
		},
		{
			name: "bad request - invalid sub_issue_id",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusBadRequest, `{"message": "Invalid sub_issue_id"}`),
			}),
			requestArgs: map[string]any{
				"method":       "remove",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(-1),
			},
			expectError:    false,
			expectedErrMsg: "failed to remove sub-issue",
		},
		{
			name: "repository not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
			}),
			requestArgs: map[string]any{
				"method":       "remove",
				"owner":        "nonexistent",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
			},
			expectError:    false,
			expectedErrMsg: "failed to remove sub-issue",
		},
		{
			name: "insufficient permissions",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				DeleteReposIssuesSubIssueByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusForbidden, `{"message": "Must have write access to repository"}`),
			}),
			requestArgs: map[string]any{
				"method":       "remove",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
			},
			expectError:    false,
			expectedErrMsg: "failed to remove sub-issue",
		},
		{
			name:         "missing required parameter owner",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"method":       "remove",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
			},
			expectError:    false,
			expectedErrMsg: "missing required parameter: owner",
		},
		{
			name:         "missing required parameter sub_issue_id",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"method":       "remove",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
			},
			expectError:    false,
			expectedErrMsg: "missing required parameter: sub_issue_id",
		},
	}

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

			// Create call request
			request := createMCPRequest(tc.requestArgs)

			// Call handler
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)

			// Verify results
			if tc.expectError {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.expectedErrMsg)
				return
			}

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

			require.NoError(t, err)

			// Parse the result and get the text content if no error
			textContent := getTextResult(t, result)

			// Unmarshal and verify the result
			var returnedIssue github.Issue
			err = json.Unmarshal([]byte(textContent.Text), &returnedIssue)
			require.NoError(t, err)
			assert.Equal(t, *tc.expectedIssue.Number, *returnedIssue.Number)
			assert.Equal(t, *tc.expectedIssue.Title, *returnedIssue.Title)
			assert.Equal(t, *tc.expectedIssue.Body, *returnedIssue.Body)
			assert.Equal(t, *tc.expectedIssue.State, *returnedIssue.State)
			assert.Equal(t, *tc.expectedIssue.HTMLURL, *returnedIssue.HTMLURL)
			assert.Equal(t, *tc.expectedIssue.User.Login, *returnedIssue.User.Login)
		})
	}
}

func Test_ReprioritizeSubIssue(t *testing.T) {
	// Verify tool definition once
	serverTool := SubIssueWrite(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "sub_issue_write", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "method")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_number")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "sub_issue_id")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "after_id")
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "before_id")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner", "repo", "issue_number", "sub_issue_id"})

	// Setup mock issue for success case (matches GitHub API response format - the updated parent issue)
	mockIssue := &github.Issue{
		Number:  github.Ptr(42),
		Title:   github.Ptr("Parent Issue"),
		Body:    github.Ptr("This is the parent issue with reprioritized sub-issues"),
		State:   github.Ptr("open"),
		HTMLURL: github.Ptr("https://github.com/owner/repo/issues/42"),
		User: &github.User{
			Login: github.Ptr("testuser"),
		},
		Labels: []*github.Label{
			{
				Name:        github.Ptr("enhancement"),
				Color:       github.Ptr("84b6eb"),
				Description: github.Ptr("New feature or request"),
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedIssue  *github.Issue
		expectedErrMsg string
	}{
		{
			name: "successful reprioritization with after_id",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue),
			}),
			requestArgs: map[string]any{
				"method":       "reprioritize",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
				"after_id":     float64(456),
			},
			expectError:   false,
			expectedIssue: mockIssue,
		},
		{
			name: "successful reprioritization with before_id",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusOK, mockIssue),
			}),
			requestArgs: map[string]any{
				"method":       "reprioritize",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
				"before_id":    float64(789),
			},
			expectError:   false,
			expectedIssue: mockIssue,
		},
		{
			name:         "validation error - neither after_id nor before_id specified",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"method":       "reprioritize",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
			},
			expectError:    false,
			expectedErrMsg: "either after_id or before_id must be specified",
		},
		{
			name:         "validation error - both after_id and before_id specified",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"method":       "reprioritize",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
				"after_id":     float64(456),
				"before_id":    float64(789),
			},
			expectError:    false,
			expectedErrMsg: "only one of after_id or before_id should be specified, not both",
		},
		{
			name: "parent issue not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
			}),
			requestArgs: map[string]any{
				"method":       "reprioritize",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(999),
				"sub_issue_id": float64(123),
				"after_id":     float64(456),
			},
			expectError:    false,
			expectedErrMsg: "failed to reprioritize sub-issue",
		},
		{
			name: "sub-issue not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusNotFound, `{"message": "Sub-issue not found"}`),
			}),
			requestArgs: map[string]any{
				"method":       "reprioritize",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(999),
				"after_id":     float64(456),
			},
			expectError:    false,
			expectedErrMsg: "failed to reprioritize sub-issue",
		},
		{
			name: "validation failed - positioning sub-issue not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusUnprocessableEntity, `{"message": "Validation failed", "errors": [{"message": "Positioning sub-issue not found"}]}`),
			}),
			requestArgs: map[string]any{
				"method":       "reprioritize",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
				"after_id":     float64(999),
			},
			expectError:    false,
			expectedErrMsg: "failed to reprioritize sub-issue",
		},
		{
			name: "insufficient permissions",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusForbidden, `{"message": "Must have write access to repository"}`),
			}),
			requestArgs: map[string]any{
				"method":       "reprioritize",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
				"after_id":     float64(456),
			},
			expectError:    false,
			expectedErrMsg: "failed to reprioritize sub-issue",
		},
		{
			name: "service unavailable",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchReposIssuesSubIssuesPriorityByOwnerByRepoByIssueNumber: mockResponse(t, http.StatusServiceUnavailable, `{"message": "Service Unavailable"}`),
			}),
			requestArgs: map[string]any{
				"method":       "reprioritize",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
				"before_id":    float64(456),
			},
			expectError:    false,
			expectedErrMsg: "failed to reprioritize sub-issue",
		},
		{
			name:         "missing required parameter owner",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"method":       "reprioritize",
				"repo":         "repo",
				"issue_number": float64(42),
				"sub_issue_id": float64(123),
				"after_id":     float64(456),
			},
			expectError:    false,
			expectedErrMsg: "missing required parameter: owner",
		},
		{
			name:         "missing required parameter sub_issue_id",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"method":       "reprioritize",
				"owner":        "owner",
				"repo":         "repo",
				"issue_number": float64(42),
				"after_id":     float64(456),
			},
			expectError:    false,
			expectedErrMsg: "missing required parameter: sub_issue_id",
		},
	}

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

			// Create call request
			request := createMCPRequest(tc.requestArgs)

			// Call handler
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)

			// Verify results
			if tc.expectError {
				require.Error(t, err)
				assert.Contains(t, err.Error(), tc.expectedErrMsg)
				return
			}

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

			require.NoError(t, err)

			// Parse the result and get the text content if no error
			textContent := getTextResult(t, result)

			// Unmarshal and verify the result
			var returnedIssue github.Issue
			err = json.Unmarshal([]byte(textContent.Text), &returnedIssue)
			require.NoError(t, err)
			assert.Equal(t, *tc.expectedIssue.Number, *returnedIssue.Number)
			assert.Equal(t, *tc.expectedIssue.Title, *returnedIssue.Title)
			assert.Equal(t, *tc.expectedIssue.Body, *returnedIssue.Body)
			assert.Equal(t, *tc.expectedIssue.State, *returnedIssue.State)
			assert.Equal(t, *tc.expectedIssue.HTMLURL, *returnedIssue.HTMLURL)
			assert.Equal(t, *tc.expectedIssue.User.Login, *returnedIssue.User.Login)
		})
	}
}

func Test_ListIssueTypes(t *testing.T) {
	// Verify tool definition once
	serverTool := ListIssueTypes(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "list_issue_types", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
	assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner"})

	// Setup mock issue types for success case
	mockIssueTypes := []*github.IssueType{
		{
			ID:          github.Ptr(int64(1)),
			Name:        github.Ptr("bug"),
			Description: github.Ptr("Something isn't working"),
			Color:       github.Ptr("d73a4a"),
		},
		{
			ID:          github.Ptr(int64(2)),
			Name:        github.Ptr("feature"),
			Description: github.Ptr("New feature or enhancement"),
			Color:       github.Ptr("a2eeef"),
		},
	}

	tests := []struct {
		name               string
		mockedClient       *http.Client
		requestArgs        map[string]any
		expectError        bool
		expectedIssueTypes []*github.IssueType
		expectedErrMsg     string
	}{
		{
			name: "successful issue types retrieval",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				"GET /orgs/testorg/issue-types": mockResponse(t, http.StatusOK, mockIssueTypes),
			}),
			requestArgs: map[string]any{
				"owner": "testorg",
			},
			expectError:        false,
			expectedIssueTypes: mockIssueTypes,
		},
		{
			name: "organization not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				"GET /orgs/nonexistent/issue-types": mockResponse(t, http.StatusNotFound, `{"message": "Organization not found"}`),
			}),
			requestArgs: map[string]any{
				"owner": "nonexistent",
			},
			expectError:    true,
			expectedErrMsg: "failed to list issue types",
		},
		{
			name: "missing owner parameter",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				"GET /orgs/testorg/issue-types": mockResponse(t, http.StatusOK, mockIssueTypes),
			}),
			requestArgs:    map[string]any{},
			expectError:    false, // This should be handled by parameter validation, error returned in result
			expectedErrMsg: "missing required parameter: owner",
		},
		{
			name: "successful repo issue types retrieval",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				"GET /repos/testorg/testrepo/issue-types": mockResponse(t, http.StatusOK, mockIssueTypes),
			}),
			requestArgs: map[string]any{
				"owner": "testorg",
				"repo":  "testrepo",
			},
			expectError:        false,
			expectedIssueTypes: mockIssueTypes,
		},
		{
			name: "repo not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				"GET /repos/testorg/nonexistent/issue-types": mockResponse(t, http.StatusNotFound, `{"message": "Not Found"}`),
			}),
			requestArgs: map[string]any{
				"owner": "testorg",
				"repo":  "nonexistent",
			},
			expectError:    true,
			expectedErrMsg: "failed to list issue types",
		},
	}

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

			// Create call request
			request := createMCPRequest(tc.requestArgs)

			// Call handler
			result, err := handler(ContextWithDeps(context.Background(), deps), &request)

			// Verify results
			if tc.expectError {
				if err != nil {
					assert.Contains(t, err.Error(), tc.expectedErrMsg)
					return
				}
				// Check if error is returned as tool result error
				require.NotNil(t, result)
				require.True(t, result.IsError)
				errorContent := getErrorResult(t, result)
				assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
				return
			}

			// Check if it's a parameter validation error (returned as tool result error)
			if result != nil && result.IsError {
				errorContent := getErrorResult(t, result)
				if tc.expectedErrMsg != "" && strings.Contains(errorContent.Text, tc.expectedErrMsg) {
					return // This is expected for parameter validation errors
				}
			}

			require.NoError(t, err)
			require.NotNil(t, result)
			require.False(t, result.IsError)
			textContent := getTextResult(t, result)

			// Unmarshal and verify the result
			var returnedIssueTypes []*github.IssueType
			err = json.Unmarshal([]byte(textContent.Text), &returnedIssueTypes)
			require.NoError(t, err)

			if tc.expectedIssueTypes != nil {
				require.Equal(t, len(tc.expectedIssueTypes), len(returnedIssueTypes))
				for i, expected := range tc.expectedIssueTypes {
					assert.Equal(t, *expected.Name, *returnedIssueTypes[i].Name)
					assert.Equal(t, *expected.Description, *returnedIssueTypes[i].Description)
					assert.Equal(t, *expected.Color, *returnedIssueTypes[i].Color)
					assert.Equal(t, *expected.ID, *returnedIssueTypes[i].ID)
				}
			}
		})
	}
}
