package github

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

	"github.com/github/github-mcp-server/internal/toolsnaps"
	"github.com/github/github-mcp-server/pkg/translations"
	"github.com/google/go-github/v89/github"
	"github.com/google/jsonschema-go/jsonschema"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

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

	assert.Equal(t, "search_repositories", tool.Name)
	assert.NotEmpty(t, tool.Description)

	schema, ok := tool.InputSchema.(*jsonschema.Schema)
	require.True(t, ok, "InputSchema should be *jsonschema.Schema")
	assert.Contains(t, schema.Properties, "query")
	assert.Contains(t, schema.Properties, "sort")
	assert.Contains(t, schema.Properties, "order")
	assert.Contains(t, schema.Properties, "page")
	assert.Contains(t, schema.Properties, "perPage")
	assert.ElementsMatch(t, schema.Required, []string{"query"})

	// Setup mock search results
	mockSearchResult := &github.RepositoriesSearchResult{
		Total:             github.Ptr(2),
		IncompleteResults: github.Ptr(false),
		Repositories: []*github.Repository{
			{
				ID:              github.Ptr(int64(12345)),
				Name:            github.Ptr("repo-1"),
				FullName:        github.Ptr("owner/repo-1"),
				HTMLURL:         github.Ptr("https://github.com/owner/repo-1"),
				Description:     github.Ptr("Test repository 1"),
				StargazersCount: github.Ptr(100),
			},
			{
				ID:              github.Ptr(int64(67890)),
				Name:            github.Ptr("repo-2"),
				FullName:        github.Ptr("owner/repo-2"),
				HTMLURL:         github.Ptr("https://github.com/owner/repo-2"),
				Description:     github.Ptr("Test repository 2"),
				StargazersCount: github.Ptr(50),
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedResult *github.RepositoriesSearchResult
		expectedErrMsg string
	}{
		{
			name: "successful repository search",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchRepositories: expectQueryParams(t, map[string]string{
					"q":        "golang test",
					"sort":     "stars",
					"order":    "desc",
					"page":     "2",
					"per_page": "10",
				}).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query":   "golang test",
				"sort":    "stars",
				"order":   "desc",
				"page":    float64(2),
				"perPage": float64(10),
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "repository search with default pagination",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchRepositories: expectQueryParams(t, map[string]string{
					"q":        "golang test",
					"page":     "1",
					"per_page": "30",
				}).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "golang test",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "search fails",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchRepositories: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusBadRequest)
					_, _ = w.Write([]byte(`{"message": "Invalid query"}`))
				}),
			}),
			requestArgs: map[string]any{
				"query": "invalid:query",
			},
			expectError:    true,
			expectedErrMsg: "failed to search repositories",
		},
	}

	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)
				require.True(t, result.IsError)
				errorContent := getErrorResult(t, result)
				assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
				return
			}

			require.NoError(t, err)
			require.False(t, result.IsError)

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

			// Unmarshal and verify the result
			var returnedResult MinimalSearchRepositoriesResult
			err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
			require.NoError(t, err)
			assert.Equal(t, *tc.expectedResult.Total, returnedResult.TotalCount)
			assert.Equal(t, *tc.expectedResult.IncompleteResults, returnedResult.IncompleteResults)
			assert.Len(t, returnedResult.Items, len(tc.expectedResult.Repositories))
			for i, repo := range returnedResult.Items {
				assert.Equal(t, *tc.expectedResult.Repositories[i].ID, repo.ID)
				assert.Equal(t, *tc.expectedResult.Repositories[i].Name, repo.Name)
				assert.Equal(t, *tc.expectedResult.Repositories[i].FullName, repo.FullName)
				assert.Equal(t, *tc.expectedResult.Repositories[i].HTMLURL, repo.HTMLURL)
			}
		})
	}
}

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

	serverTool := SearchRepositories(translations.NullTranslationHelper)

	type repoFixture struct {
		owner     string
		name      string
		isPrivate bool
	}

	makeRepo := func(r repoFixture) *github.Repository {
		return &github.Repository{
			ID:       github.Ptr(int64(1)),
			Name:     github.Ptr(r.name),
			FullName: github.Ptr(r.owner + "/" + r.name),
			Private:  github.Ptr(r.isPrivate),
			Owner:    &github.User{Login: github.Ptr(r.owner)},
		}
	}

	makeMockClient := func(repos []repoFixture) *http.Client {
		searchResult := &github.RepositoriesSearchResult{
			Total:             github.Ptr(len(repos)),
			IncompleteResults: github.Ptr(false),
		}
		for _, r := range repos {
			searchResult.Repositories = append(searchResult.Repositories, makeRepo(r))
		}
		return MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
			GetSearchRepositories: mockResponse(t, http.StatusOK, searchResult),
		})
	}

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

	t.Run("insiders mode disabled omits ifc label", func(t *testing.T) {
		deps := BaseDeps{
			Client: mustNewGHClient(t, makeMockClient([]repoFixture{{owner: "octocat", name: "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) {
		deps := BaseDeps{
			Client: mustNewGHClient(t, makeMockClient([]repoFixture{
				{owner: "octocat", name: "public-a"},
				{owner: "octocat", name: "public-b"},
			})),
			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) {
		deps := BaseDeps{
			Client: mustNewGHClient(t, makeMockClient([]repoFixture{
				{owner: "octocat", name: "private-repo", isPrivate: true},
				{owner: "octocat", name: "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 empty results emits public untrusted", func(t *testing.T) {
		deps := BaseDeps{
			Client:         mustNewGHClient(t, makeMockClient(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 Test_SearchRepositories_FullOutput(t *testing.T) {
	mockSearchResult := &github.RepositoriesSearchResult{
		Total:             github.Ptr(1),
		IncompleteResults: github.Ptr(false),
		Repositories: []*github.Repository{
			{
				ID:              github.Ptr(int64(12345)),
				Name:            github.Ptr("test-repo"),
				FullName:        github.Ptr("owner/test-repo"),
				HTMLURL:         github.Ptr("https://github.com/owner/test-repo"),
				Description:     github.Ptr("Test repository"),
				StargazersCount: github.Ptr(100),
			},
		},
	}

	mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		GetSearchRepositories: expectQueryParams(t, map[string]string{
			"q":        "golang test",
			"page":     "1",
			"per_page": "30",
		}).andThen(
			mockResponse(t, http.StatusOK, mockSearchResult),
		),
	})

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

	args := map[string]any{
		"query":          "golang test",
		"minimal_output": false,
	}

	request := createMCPRequest(args)

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

	require.NoError(t, err)
	require.False(t, result.IsError)

	textContent := getTextResult(t, result)

	// Unmarshal as full GitHub API response
	var returnedResult github.RepositoriesSearchResult
	err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
	require.NoError(t, err)

	// Verify it's the full API response, not minimal
	assert.Equal(t, *mockSearchResult.Total, *returnedResult.Total)
	assert.Equal(t, *mockSearchResult.IncompleteResults, *returnedResult.IncompleteResults)
	assert.Len(t, returnedResult.Repositories, 1)
	assert.Equal(t, *mockSearchResult.Repositories[0].ID, *returnedResult.Repositories[0].ID)
	assert.Equal(t, *mockSearchResult.Repositories[0].Name, *returnedResult.Repositories[0].Name)
}

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

	assert.Equal(t, "search_code", tool.Name)
	assert.NotEmpty(t, tool.Description)

	schema, ok := tool.InputSchema.(*jsonschema.Schema)
	require.True(t, ok, "InputSchema should be *jsonschema.Schema")
	assert.Contains(t, schema.Properties, "query")
	assert.Contains(t, schema.Properties, "sort")
	assert.Contains(t, schema.Properties, "order")
	assert.Contains(t, schema.Properties, "perPage")
	assert.Contains(t, schema.Properties, "page")
	assert.Contains(t, schema.Properties, "fields")
	assert.ElementsMatch(t, schema.Required, []string{"query"})

	// Setup mock search results
	mockSearchResult := &github.CodeSearchResult{
		Total:             github.Ptr(2),
		IncompleteResults: github.Ptr(false),
		CodeResults: []*github.CodeResult{
			{
				Name: github.Ptr("file1.go"),
				Path: github.Ptr("path/to/file1.go"),
				SHA:  github.Ptr("abc123def456"),
				Repository: &github.Repository{
					Name:     github.Ptr("repo"),
					FullName: github.Ptr("owner/repo"),
				},
				TextMatches: []*github.TextMatch{
					{
						Fragment: github.Ptr("func main() { fmt.Println(\"hello\") }"),
					},
				},
			},
			{
				Name: github.Ptr("file2.go"),
				Path: github.Ptr("path/to/file2.go"),
				SHA:  github.Ptr("def456abc123"),
				Repository: &github.Repository{
					Name:     github.Ptr("repo"),
					FullName: github.Ptr("owner/repo"),
				},
			},
		},
	}

	textMatchAcceptHeader := map[string]string{
		"Accept": "text-match",
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedResult *github.CodeSearchResult
		expectedErrMsg string
	}{
		{
			name: "successful code search with all parameters",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchCode: expectQueryParams(t, map[string]string{
					"q":        "fmt.Println language:go",
					"sort":     "indexed",
					"order":    "desc",
					"page":     "1",
					"per_page": "30",
				}).withHeaders(textMatchAcceptHeader).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query":   "fmt.Println language:go",
				"sort":    "indexed",
				"order":   "desc",
				"page":    float64(1),
				"perPage": float64(30),
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "code search with minimal parameters",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchCode: expectQueryParams(t, map[string]string{
					"q":        "fmt.Println language:go",
					"page":     "1",
					"per_page": "30",
				}).withHeaders(textMatchAcceptHeader).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "fmt.Println language:go",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "search code fails",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchCode: 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 code",
		},
	}

	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)
				require.True(t, result.IsError)
				errorContent := getErrorResult(t, result)
				assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
				return
			}

			require.NoError(t, err)
			require.False(t, result.IsError)

			textContent := getTextResult(t, result)

			var returnedResult MinimalCodeSearchResult
			err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
			require.NoError(t, err)
			assert.Equal(t, *tc.expectedResult.Total, returnedResult.TotalCount)
			assert.Equal(t, *tc.expectedResult.IncompleteResults, returnedResult.IncompleteResults)
			assert.Len(t, returnedResult.Items, len(tc.expectedResult.CodeResults))
			for i, code := range returnedResult.Items {
				assert.Equal(t, tc.expectedResult.CodeResults[i].GetName(), code.Name)
				assert.Equal(t, tc.expectedResult.CodeResults[i].GetPath(), code.Path)
				assert.Equal(t, tc.expectedResult.CodeResults[i].GetSHA(), code.SHA)
				assert.Equal(t, tc.expectedResult.CodeResults[i].Repository.GetFullName(), code.Repository)
			}

			// Verify text matches are included when present
			if len(tc.expectedResult.CodeResults[0].TextMatches) > 0 {
				require.NotEmpty(t, returnedResult.Items[0].TextMatches)
				assert.Equal(t,
					tc.expectedResult.CodeResults[0].TextMatches[0].GetFragment(),
					returnedResult.Items[0].TextMatches[0].GetFragment(),
				)
			}
		})
	}
}

func Test_SearchCode_FieldFiltering(t *testing.T) {
	mockSearchResult := &github.CodeSearchResult{
		Total:             github.Ptr(1),
		IncompleteResults: github.Ptr(false),
		CodeResults: []*github.CodeResult{
			{
				Name: github.Ptr("file1.go"),
				Path: github.Ptr("path/to/file1.go"),
				SHA:  github.Ptr("abc123def456"),
				Repository: &github.Repository{
					Name:     github.Ptr("repo"),
					FullName: github.Ptr("owner/repo"),
				},
				TextMatches: []*github.TextMatch{
					{Fragment: github.Ptr("func main() {}")},
				},
			},
		},
	}

	serverTool := SearchCode(translations.NullTranslationHelper)
	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		GetSearchCode: mockResponse(t, http.StatusOK, mockSearchResult),
	}))
	deps := BaseDeps{Client: client}
	handler := serverTool.Handler(deps)

	request := createMCPRequest(map[string]any{
		"query":  "fmt.Println language:go",
		"fields": []any{"name", "path"},
	})

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

	textContent := getTextResult(t, result)

	// The wrapper metadata is preserved while each item is reduced to the
	// requested fields only; the heavier repository and text_matches data is
	// dropped.
	var returned struct {
		TotalCount        int              `json:"total_count"`
		IncompleteResults bool             `json:"incomplete_results"`
		Items             []map[string]any `json:"items"`
	}
	require.NoError(t, json.Unmarshal([]byte(textContent.Text), &returned))
	assert.Equal(t, 1, returned.TotalCount)
	require.Len(t, returned.Items, 1)
	require.Len(t, returned.Items[0], 2)
	assert.Contains(t, returned.Items[0], "name")
	assert.Contains(t, returned.Items[0], "path")

	assert.NotContains(t, textContent.Text, "repository")
	assert.NotContains(t, textContent.Text, "text_matches")
}

func Test_LegacySearchCode_Definition(t *testing.T) {
	serverTool := LegacySearchCode(translations.NullTranslationHelper)
	tool := serverTool.Tool
	// LegacySearchCode is the FeatureFlagFieldsParam-disabled variant and owns
	// the canonical search_code.snap (no `fields`).
	require.NoError(t, toolsnaps.Test(tool.Name, tool))
	require.Equal(t, []string{FeatureFlagFieldsParam}, serverTool.FeatureFlagDisable)

	assert.Equal(t, "search_code", tool.Name)
	schema, ok := tool.InputSchema.(*jsonschema.Schema)
	require.True(t, ok, "InputSchema should be *jsonschema.Schema")
	assert.NotContains(t, schema.Properties, "fields")
}

func Test_SearchCode_FieldsTelemetry(t *testing.T) {
	mockSearchResult := &github.CodeSearchResult{
		Total:             github.Ptr(1),
		IncompleteResults: github.Ptr(false),
		CodeResults: []*github.CodeResult{
			{
				Name: github.Ptr("file1.go"),
				Path: github.Ptr("path/to/file1.go"),
				SHA:  github.Ptr("abc123def456"),
				Repository: &github.Repository{
					Name:     github.Ptr("repo"),
					FullName: github.Ptr("owner/repo"),
				},
				TextMatches: []*github.TextMatch{
					{Fragment: github.Ptr("func main() {}")},
				},
			},
		},
	}

	serverTool := SearchCode(translations.NullTranslationHelper)
	client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
		GetSearchCode: mockResponse(t, http.StatusOK, mockSearchResult),
	}))

	t.Run("filtered call records savings", func(t *testing.T) {
		deps, rec := depsWithRecordingMetrics(t, BaseDeps{Client: client})
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"query":  "fmt.Println language:go",
			"fields": []any{"name", "path"},
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		require.False(t, result.IsError)

		call, ok := rec.increment(metricFieldsToolCall)
		require.True(t, ok)
		assert.Equal(t, "search_code", call.tags["tool"])
		assert.Equal(t, "true", call.tags["filtered"])

		full, ok := rec.counter(metricFieldsBytesFull)
		require.True(t, ok)
		sent, ok := rec.counter(metricFieldsBytesSent)
		require.True(t, ok)
		assert.Greater(t, full.value, sent.value, "filtering should remove bytes")
	})

	t.Run("unfiltered call records adoption only", func(t *testing.T) {
		deps, rec := depsWithRecordingMetrics(t, BaseDeps{Client: client})
		handler := serverTool.Handler(deps)

		request := createMCPRequest(map[string]any{
			"query": "fmt.Println language:go",
		})
		result, err := handler(ContextWithDeps(context.Background(), deps), &request)
		require.NoError(t, err)
		require.False(t, result.IsError)

		call, ok := rec.increment(metricFieldsToolCall)
		require.True(t, ok)
		assert.Equal(t, "false", call.tags["filtered"])

		_, ok = rec.counter(metricFieldsBytesFull)
		assert.False(t, ok, "no byte counters when not filtered")
	})
}

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

	assert.Equal(t, "search_users", tool.Name)
	assert.NotEmpty(t, tool.Description)

	schema, ok := tool.InputSchema.(*jsonschema.Schema)
	require.True(t, ok, "InputSchema should be *jsonschema.Schema")
	assert.Contains(t, schema.Properties, "query")
	assert.Contains(t, schema.Properties, "sort")
	assert.Contains(t, schema.Properties, "order")
	assert.Contains(t, schema.Properties, "perPage")
	assert.Contains(t, schema.Properties, "page")
	assert.ElementsMatch(t, schema.Required, []string{"query"})

	// Setup mock search results
	mockSearchResult := &github.UsersSearchResult{
		Total:             github.Ptr(2),
		IncompleteResults: github.Ptr(false),
		Users: []*github.User{
			{
				Login:     github.Ptr("user1"),
				ID:        github.Ptr(int64(1001)),
				HTMLURL:   github.Ptr("https://github.com/user1"),
				AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/1001"),
			},
			{
				Login:     github.Ptr("user2"),
				ID:        github.Ptr(int64(1002)),
				HTMLURL:   github.Ptr("https://github.com/user2"),
				AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/1002"),
				Type:      github.Ptr("User"),
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedResult *github.UsersSearchResult
		expectedErrMsg string
	}{
		{
			name: "successful users search with all parameters",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchUsers: expectQueryParams(t, map[string]string{
					"q":        "type:user location:finland language:go",
					"sort":     "followers",
					"order":    "desc",
					"page":     "1",
					"per_page": "30",
				}).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query":   "location:finland language:go",
				"sort":    "followers",
				"order":   "desc",
				"page":    float64(1),
				"perPage": float64(30),
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "users search with minimal parameters",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchUsers: expectQueryParams(t, map[string]string{
					"q":        "type:user location:finland language:go",
					"page":     "1",
					"per_page": "30",
				}).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "location:finland language:go",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "query with existing type:user filter - no duplication",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchUsers: expectQueryParams(t, map[string]string{
					"q":        "type:user location:seattle followers:>100",
					"page":     "1",
					"per_page": "30",
				}).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "type:user location:seattle followers:>100",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "complex query with existing type:user filter and OR operators",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchUsers: expectQueryParams(t, map[string]string{
					"q":        "type:user (location:seattle OR location:california) followers:>50",
					"page":     "1",
					"per_page": "30",
				}).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "type:user (location:seattle OR location:california) followers:>50",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "search users fails",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchUsers: 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 users",
		},
	}

	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)
				require.True(t, result.IsError)
				errorContent := getErrorResult(t, result)
				assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
				return
			}

			require.NoError(t, err)
			require.False(t, result.IsError)

			// Parse the result and get the text content if no error
			require.NotNil(t, result)

			textContent := getTextResult(t, result)

			// Unmarshal and verify the result
			var returnedResult MinimalSearchUsersResult
			err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
			require.NoError(t, err)
			assert.Equal(t, *tc.expectedResult.Total, returnedResult.TotalCount)
			assert.Equal(t, *tc.expectedResult.IncompleteResults, returnedResult.IncompleteResults)
			assert.Len(t, returnedResult.Items, len(tc.expectedResult.Users))
			for i, user := range returnedResult.Items {
				assert.Equal(t, *tc.expectedResult.Users[i].Login, user.Login)
				assert.Equal(t, *tc.expectedResult.Users[i].ID, user.ID)
				assert.Equal(t, *tc.expectedResult.Users[i].HTMLURL, user.ProfileURL)
				assert.Equal(t, *tc.expectedResult.Users[i].AvatarURL, user.AvatarURL)
			}
		})
	}
}

func Test_SearchOrgs(t *testing.T) {
	// Verify tool definition once
	serverTool := SearchOrgs(translations.NullTranslationHelper)
	tool := serverTool.Tool

	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "search_orgs", tool.Name)
	assert.NotEmpty(t, tool.Description)

	schema, ok := tool.InputSchema.(*jsonschema.Schema)
	require.True(t, ok, "InputSchema should be *jsonschema.Schema")
	assert.Contains(t, schema.Properties, "query")
	assert.Contains(t, schema.Properties, "sort")
	assert.Contains(t, schema.Properties, "order")
	assert.Contains(t, schema.Properties, "perPage")
	assert.Contains(t, schema.Properties, "page")
	assert.ElementsMatch(t, schema.Required, []string{"query"})

	// Setup mock search results
	mockSearchResult := &github.UsersSearchResult{
		Total:             github.Ptr(int(2)),
		IncompleteResults: github.Ptr(false),
		Users: []*github.User{
			{
				Login:     github.Ptr("org-1"),
				ID:        github.Ptr(int64(111)),
				HTMLURL:   github.Ptr("https://github.com/org-1"),
				AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/111?v=4"),
			},
			{
				Login:     github.Ptr("org-2"),
				ID:        github.Ptr(int64(222)),
				HTMLURL:   github.Ptr("https://github.com/org-2"),
				AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/222?v=4"),
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedResult *github.UsersSearchResult
		expectedErrMsg string
	}{
		{
			name: "successful org search",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchUsers: expectQueryParams(t, map[string]string{
					"q":        "type:org github",
					"page":     "1",
					"per_page": "30",
				}).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "github",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "query with existing type:org filter - no duplication",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchUsers: expectQueryParams(t, map[string]string{
					"q":        "type:org location:california followers:>1000",
					"page":     "1",
					"per_page": "30",
				}).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "type:org location:california followers:>1000",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "complex query with existing type:org filter and OR operators",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchUsers: expectQueryParams(t, map[string]string{
					"q":        "type:org (location:seattle OR location:california OR location:newyork) repos:>10",
					"page":     "1",
					"per_page": "30",
				}).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "type:org (location:seattle OR location:california OR location:newyork) repos:>10",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "org search fails",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchUsers: 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 orgs",
		},
	}

	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)
				require.True(t, result.IsError)
				errorContent := getErrorResult(t, result)
				assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
				return
			}

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

			textContent := getTextResult(t, result)

			// Unmarshal and verify the result
			var returnedResult MinimalSearchUsersResult
			err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
			require.NoError(t, err)
			assert.Equal(t, *tc.expectedResult.Total, returnedResult.TotalCount)
			assert.Equal(t, *tc.expectedResult.IncompleteResults, returnedResult.IncompleteResults)
			assert.Len(t, returnedResult.Items, len(tc.expectedResult.Users))
			for i, org := range returnedResult.Items {
				assert.Equal(t, *tc.expectedResult.Users[i].Login, org.Login)
				assert.Equal(t, *tc.expectedResult.Users[i].ID, org.ID)
				assert.Equal(t, *tc.expectedResult.Users[i].HTMLURL, org.ProfileURL)
				assert.Equal(t, *tc.expectedResult.Users[i].AvatarURL, org.AvatarURL)
			}
		})
	}
}

func Test_SearchCommits(t *testing.T) {
	serverTool := SearchCommits(translations.NullTranslationHelper)
	tool := serverTool.Tool
	require.NoError(t, toolsnaps.Test(tool.Name, tool))

	assert.Equal(t, "search_commits", tool.Name)
	assert.NotEmpty(t, tool.Description)

	schema, ok := tool.InputSchema.(*jsonschema.Schema)
	require.True(t, ok, "InputSchema should be *jsonschema.Schema")
	assert.Contains(t, schema.Properties, "query")
	assert.Contains(t, schema.Properties, "sort")
	assert.Contains(t, schema.Properties, "order")
	assert.Contains(t, schema.Properties, "page")
	assert.Contains(t, schema.Properties, "perPage")
	assert.ElementsMatch(t, schema.Required, []string{"query"})

	now := time.Now().Truncate(time.Second)
	mockSearchResult := &github.CommitsSearchResult{
		Total:             github.Ptr(2),
		IncompleteResults: github.Ptr(false),
		Commits: []*github.CommitResult{
			{
				SHA:     github.Ptr("abc123commit"),
				HTMLURL: github.Ptr("https://github.com/owner/repo/commit/abc123commit"),
				Commit: &github.Commit{
					Message: github.Ptr("Initial commit"),
					Author: &github.CommitAuthor{
						Name:  github.Ptr("Author Name"),
						Email: github.Ptr("author@example.com"),
						Date:  &github.Timestamp{Time: now},
					},
				},
				Author: &github.User{
					Login:   github.Ptr("author"),
					ID:      github.Ptr(int64(1)),
					HTMLURL: github.Ptr("https://github.com/author"),
				},
				Repository: &github.Repository{
					FullName: github.Ptr("owner/repo"),
					HTMLURL:  github.Ptr("https://github.com/owner/repo"),
					Private:  github.Ptr(false),
				},
			},
			{
				// Commit with no resolved GitHub user for author or committer
				// (common when the commit email isn't linked to an account).
				SHA:     github.Ptr("def456commit"),
				HTMLURL: github.Ptr("https://github.com/owner/repo/commit/def456commit"),
				Commit: &github.Commit{
					Message: github.Ptr("Unlinked author"),
				},
				Repository: &github.Repository{
					FullName: github.Ptr("owner/repo"),
				},
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedResult *github.CommitsSearchResult
		expectedErrMsg string
	}{
		{
			name: "successful commit search",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchCommits: expectQueryParams(t, map[string]string{
					"q":        "fix bug in:message repo:owner/repo",
					"sort":     "author-date",
					"order":    "desc",
					"page":     "1",
					"per_page": "30",
				}).andThen(
					mockResponse(t, http.StatusOK, mockSearchResult),
				),
			}),
			requestArgs: map[string]any{
				"query": "fix bug in:message repo:owner/repo",
				"sort":  "author-date",
				"order": "desc",
			},
			expectError:    false,
			expectedResult: mockSearchResult,
		},
		{
			name: "search fails",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetSearchCommits: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusUnprocessableEntity)
					_, _ = w.Write([]byte(`{"message": "Validation Failed"}`))
				}),
			}),
			requestArgs: map[string]any{
				"query": "invalid:syntax",
			},
			expectError:    true,
			expectedErrMsg: "failed to search commits",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			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)

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

			require.NoError(t, err)
			require.False(t, result.IsError)

			textContent := getTextResult(t, result)
			var returnedResult MinimalSearchCommitsResult
			err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
			require.NoError(t, err)

			assert.Equal(t, tc.expectedResult.GetTotal(), returnedResult.TotalCount)
			assert.Len(t, returnedResult.Items, len(tc.expectedResult.Commits))
			assert.Equal(t, *tc.expectedResult.Commits[0].SHA, returnedResult.Items[0].SHA)
			assert.Equal(t, *tc.expectedResult.Commits[0].Commit.Message, returnedResult.Items[0].Commit.Message)
			assert.Equal(t, *tc.expectedResult.Commits[0].Commit.Author.Name, returnedResult.Items[0].Commit.Author.Name)
			assert.Equal(t, now.Format(time.RFC3339), returnedResult.Items[0].Commit.Author.Date)
			assert.Equal(t, *tc.expectedResult.Commits[0].Author.Login, returnedResult.Items[0].Author.Login)

			// Repository info is required so callers can identify which repo
			// each cross-repo search result belongs to.
			require.NotNil(t, returnedResult.Items[0].Repository)
			assert.Equal(t, "owner/repo", returnedResult.Items[0].Repository.FullName)
			assert.Equal(t, "https://github.com/owner/repo", returnedResult.Items[0].Repository.HTMLURL)

			// Second commit has no resolved GitHub user for author/committer
			// and no commit-level author block — the handler must not panic
			// and must omit those fields cleanly.
			require.Len(t, returnedResult.Items, 2)
			assert.Equal(t, "def456commit", returnedResult.Items[1].SHA)
			assert.Nil(t, returnedResult.Items[1].Author)
			assert.Nil(t, returnedResult.Items[1].Committer)
			require.NotNil(t, returnedResult.Items[1].Commit)
			assert.Nil(t, returnedResult.Items[1].Commit.Author)
			assert.Nil(t, returnedResult.Items[1].Commit.Committer)
		})
	}
}
