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_ListGists(t *testing.T) {
	// Verify tool definition
	serverTool := ListGists(translations.NullTranslationHelper)
	tool := serverTool.Tool

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

	assert.Equal(t, "list_gists", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.True(t, tool.Annotations.ReadOnlyHint, "list_gists tool should be read-only")

	schema, ok := tool.InputSchema.(*jsonschema.Schema)
	require.True(t, ok, "InputSchema should be *jsonschema.Schema")
	assert.Contains(t, schema.Properties, "username")
	assert.Contains(t, schema.Properties, "since")
	assert.Contains(t, schema.Properties, "page")
	assert.Contains(t, schema.Properties, "perPage")
	assert.Empty(t, schema.Required)

	// Setup mock gists for success case
	mockGists := []*github.Gist{
		{
			ID:          github.Ptr("gist1"),
			Description: github.Ptr("First Gist"),
			HTMLURL:     github.Ptr("https://gist.github.com/user/gist1"),
			Public:      github.Ptr(true),
			CreatedAt:   &github.Timestamp{Time: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)},
			Owner:       &github.User{Login: github.Ptr("user")},
			Files: map[github.GistFilename]github.GistFile{
				"file1.txt": {
					Filename: github.Ptr("file1.txt"),
					Content:  github.Ptr("content of file 1"),
				},
			},
		},
		{
			ID:          github.Ptr("gist2"),
			Description: github.Ptr("Second Gist"),
			HTMLURL:     github.Ptr("https://gist.github.com/testuser/gist2"),
			Public:      github.Ptr(false),
			CreatedAt:   &github.Timestamp{Time: time.Date(2023, 2, 1, 0, 0, 0, 0, time.UTC)},
			Owner:       &github.User{Login: github.Ptr("testuser")},
			Files: map[github.GistFilename]github.GistFile{
				"file2.js": {
					Filename: github.Ptr("file2.js"),
					Content:  github.Ptr("console.log('hello');"),
				},
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedGists  []*github.Gist
		expectedErrMsg string
	}{
		{
			name: "list authenticated user's gists",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetGists: mockResponse(t, http.StatusOK, mockGists),
			}),
			requestArgs:   map[string]any{},
			expectError:   false,
			expectedGists: mockGists,
		},
		{
			name: "list specific user's gists",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetUsersGistsByUsername: mockResponse(t, http.StatusOK, mockGists),
			}),
			requestArgs: map[string]any{
				"username": "testuser",
			},
			expectError:   false,
			expectedGists: mockGists,
		},
		{
			name: "list gists with pagination and since parameter",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetGists: expectQueryParams(t, map[string]string{
					"since":    "2023-01-01T00:00:00Z",
					"page":     "2",
					"per_page": "5",
				}).andThen(
					mockResponse(t, http.StatusOK, mockGists),
				),
			}),
			requestArgs: map[string]any{
				"since":   "2023-01-01T00:00:00Z",
				"page":    float64(2),
				"perPage": float64(5),
			},
			expectError:   false,
			expectedGists: mockGists,
		},
		{
			name: "invalid since parameter",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetGists: mockResponse(t, http.StatusOK, mockGists),
			}),
			requestArgs: map[string]any{
				"since": "invalid-date",
			},
			expectError:    true,
			expectedErrMsg: "invalid since timestamp",
		},
		{
			name: "list gists fails with error",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetGists: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusUnauthorized)
					_, _ = w.Write([]byte(`{"message": "Requires authentication"}`))
				}),
			}),
			requestArgs:    map[string]any{},
			expectError:    true,
			expectedErrMsg: "failed to list gists",
		},
	}

	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)
			require.NoError(t, err)

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

			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 returnedGists []*github.Gist
			err = json.Unmarshal([]byte(textContent.Text), &returnedGists)
			require.NoError(t, err)

			assert.Len(t, returnedGists, len(tc.expectedGists))
			for i, gist := range returnedGists {
				assert.Equal(t, *tc.expectedGists[i].ID, *gist.ID)
				assert.Equal(t, *tc.expectedGists[i].Description, *gist.Description)
				assert.Equal(t, *tc.expectedGists[i].HTMLURL, *gist.HTMLURL)
				assert.Equal(t, *tc.expectedGists[i].Public, *gist.Public)
			}
		})
	}
}

func Test_GetGist(t *testing.T) {
	// Verify tool definition
	serverTool := GetGist(translations.NullTranslationHelper)
	tool := serverTool.Tool

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

	assert.Equal(t, "get_gist", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.True(t, tool.Annotations.ReadOnlyHint, "get_gist tool should be read-only")

	schema, ok := tool.InputSchema.(*jsonschema.Schema)
	require.True(t, ok, "InputSchema should be *jsonschema.Schema")
	assert.Contains(t, schema.Properties, "gist_id")

	assert.Contains(t, schema.Required, "gist_id")

	// Setup mock gist for success case
	mockGist := github.Gist{
		ID:          github.Ptr("gist1"),
		Description: github.Ptr("First Gist"),
		HTMLURL:     github.Ptr("https://gist.github.com/user/gist1"),
		Public:      github.Ptr(true),
		CreatedAt:   &github.Timestamp{Time: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)},
		Owner:       &github.User{Login: github.Ptr("user")},
		Files: map[github.GistFilename]github.GistFile{
			github.GistFilename("file1.txt"): {
				Filename: github.Ptr("file1.txt"),
				Content:  github.Ptr("content of file 1"),
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedGists  github.Gist
		expectedErrMsg string
	}{
		{
			name: "Successful fetching different gist",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetGistsByGistID: mockResponse(t, http.StatusOK, mockGist),
			}),
			requestArgs: map[string]any{
				"gist_id": "gist1",
			},
			expectError:   false,
			expectedGists: mockGist,
		},
		{
			name: "gist_id parameter missing",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetGistsByGistID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusUnprocessableEntity)
					_, _ = w.Write([]byte(`{"message": "Invalid Request"}`))
				}),
			}),
			requestArgs:    map[string]any{},
			expectError:    true,
			expectedErrMsg: "missing required parameter: gist_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)
			require.NoError(t, err)

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

			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 returnedGists github.Gist
			err = json.Unmarshal([]byte(textContent.Text), &returnedGists)
			require.NoError(t, err)

			assert.Equal(t, *tc.expectedGists.ID, *returnedGists.ID)
			assert.Equal(t, *tc.expectedGists.Description, *returnedGists.Description)
			assert.Equal(t, *tc.expectedGists.HTMLURL, *returnedGists.HTMLURL)
			assert.Equal(t, *tc.expectedGists.Public, *returnedGists.Public)
		})
	}
}

func Test_CreateGist(t *testing.T) {
	// Verify tool definition
	serverTool := CreateGist(translations.NullTranslationHelper)
	tool := serverTool.Tool

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

	assert.Equal(t, "create_gist", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.False(t, tool.Annotations.ReadOnlyHint, "create_gist tool should not be read-only")

	schema, ok := tool.InputSchema.(*jsonschema.Schema)
	require.True(t, ok, "InputSchema should be *jsonschema.Schema")
	assert.Contains(t, schema.Properties, "description")
	assert.Contains(t, schema.Properties, "filename")
	assert.Contains(t, schema.Properties, "content")
	assert.Contains(t, schema.Properties, "public")

	// Verify required parameters
	assert.Contains(t, schema.Required, "filename")
	assert.Contains(t, schema.Required, "content")

	// Setup mock data for test cases
	createdGist := &github.Gist{
		ID:          github.Ptr("new-gist-id"),
		Description: github.Ptr("Test Gist"),
		HTMLURL:     github.Ptr("https://gist.github.com/user/new-gist-id"),
		Public:      github.Ptr(false),
		CreatedAt:   &github.Timestamp{Time: time.Now()},
		Owner:       &github.User{Login: github.Ptr("user")},
		Files: map[github.GistFilename]github.GistFile{
			"test.go": {
				Filename: github.Ptr("test.go"),
				Content:  github.Ptr("package main\n\nfunc main() {\n\tfmt.Println(\"Hello, Gist!\")\n}"),
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedErrMsg string
		expectedGist   *github.Gist
	}{
		{
			name: "create gist successfully",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostGists: mockResponse(t, http.StatusCreated, createdGist),
			}),
			requestArgs: map[string]any{
				"filename":    "test.go",
				"content":     "package main\n\nfunc main() {\n\tfmt.Println(\"Hello, Gist!\")\n}",
				"description": "Test Gist",
				"public":      false,
			},
			expectError:  false,
			expectedGist: createdGist,
		},
		{
			name:         "missing required filename",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"content":     "test content",
				"description": "Test Gist",
			},
			expectError:    true,
			expectedErrMsg: "missing required parameter: filename",
		},
		{
			name:         "missing required content",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"filename":    "test.go",
				"description": "Test Gist",
			},
			expectError:    true,
			expectedErrMsg: "missing required parameter: content",
		},
		{
			name: "api returns error",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PostGists: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusUnauthorized)
					_, _ = w.Write([]byte(`{"message": "Requires authentication"}`))
				}),
			}),
			requestArgs: map[string]any{
				"filename":    "test.go",
				"content":     "package main",
				"description": "Test Gist",
			},
			expectError:    true,
			expectedErrMsg: "failed to create gist",
		},
	}

	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)
			require.NoError(t, err)

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

			require.False(t, result.IsError)
			assert.NotNil(t, result)

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

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

			assert.Equal(t, tc.expectedGist.GetHTMLURL(), gist.URL)
		})
	}
}

func Test_UpdateGist(t *testing.T) {
	// Verify tool definition
	serverTool := UpdateGist(translations.NullTranslationHelper)
	tool := serverTool.Tool

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

	assert.Equal(t, "update_gist", tool.Name)
	assert.NotEmpty(t, tool.Description)
	assert.False(t, tool.Annotations.ReadOnlyHint, "update_gist tool should not be read-only")

	schema, ok := tool.InputSchema.(*jsonschema.Schema)
	require.True(t, ok, "InputSchema should be *jsonschema.Schema")
	assert.Contains(t, schema.Properties, "gist_id")
	assert.Contains(t, schema.Properties, "description")
	assert.Contains(t, schema.Properties, "filename")
	assert.Contains(t, schema.Properties, "content")

	// Verify required parameters
	assert.Contains(t, schema.Required, "gist_id")
	assert.Contains(t, schema.Required, "filename")
	assert.Contains(t, schema.Required, "content")

	// Setup mock data for test cases
	updatedGist := &github.Gist{
		ID:          github.Ptr("existing-gist-id"),
		Description: github.Ptr("Updated Test Gist"),
		HTMLURL:     github.Ptr("https://gist.github.com/user/existing-gist-id"),
		Public:      github.Ptr(true),
		UpdatedAt:   &github.Timestamp{Time: time.Now()},
		Owner:       &github.User{Login: github.Ptr("user")},
		Files: map[github.GistFilename]github.GistFile{
			"updated.go": {
				Filename: github.Ptr("updated.go"),
				Content:  github.Ptr("package main\n\nfunc main() {\n\tfmt.Println(\"Updated Gist!\")\n}"),
			},
		},
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedErrMsg string
		expectedGist   *github.Gist
	}{
		{
			name: "update gist successfully",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchGistsByGistID: mockResponse(t, http.StatusOK, updatedGist),
			}),
			requestArgs: map[string]any{
				"gist_id":     "existing-gist-id",
				"filename":    "updated.go",
				"content":     "package main\n\nfunc main() {\n\tfmt.Println(\"Updated Gist!\")\n}",
				"description": "Updated Test Gist",
			},
			expectError:  false,
			expectedGist: updatedGist,
		},
		{
			name:         "missing required gist_id",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"filename":    "updated.go",
				"content":     "updated content",
				"description": "Updated Test Gist",
			},
			expectError:    true,
			expectedErrMsg: "missing required parameter: gist_id",
		},
		{
			name:         "missing required filename",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"gist_id":     "existing-gist-id",
				"content":     "updated content",
				"description": "Updated Test Gist",
			},
			expectError:    true,
			expectedErrMsg: "missing required parameter: filename",
		},
		{
			name:         "missing required content",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"gist_id":     "existing-gist-id",
				"filename":    "updated.go",
				"description": "Updated Test Gist",
			},
			expectError:    true,
			expectedErrMsg: "missing required parameter: content",
		},
		{
			name: "api returns error",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchGistsByGistID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
					w.WriteHeader(http.StatusNotFound)
					_, _ = w.Write([]byte(`{"message": "Not Found"}`))
				}),
			}),
			requestArgs: map[string]any{
				"gist_id":     "nonexistent-gist-id",
				"filename":    "updated.go",
				"content":     "package main",
				"description": "Updated Test Gist",
			},
			expectError:    true,
			expectedErrMsg: "failed to update gist",
		},
	}

	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)
			require.NoError(t, err)

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

			require.False(t, result.IsError)
			assert.NotNil(t, result)

			// 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.expectedGist.GetHTMLURL(), updateResp.URL)
		})
	}
}

func Test_UpdateGist_DescriptionOnlySentWhenProvided(t *testing.T) {
	serverTool := UpdateGist(translations.NullTranslationHelper)

	updatedGist := &github.Gist{
		ID:      github.Ptr("existing-gist-id"),
		HTMLURL: github.Ptr("https://gist.github.com/user/existing-gist-id"),
	}

	cases := []struct {
		name            string
		args            map[string]any
		wantDescPresent bool
		wantDescValue   string
	}{
		{
			name: "omitted description is not sent, preserving the existing one",
			args: map[string]any{
				"gist_id":  "existing-gist-id",
				"filename": "updated.go",
				"content":  "package main",
			},
			wantDescPresent: false,
		},
		{
			name: "explicit empty description is sent to clear it",
			args: map[string]any{
				"gist_id":     "existing-gist-id",
				"filename":    "updated.go",
				"content":     "package main",
				"description": "",
			},
			wantDescPresent: true,
			wantDescValue:   "",
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var gotBody map[string]any
			client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchGistsByGistID: func(w http.ResponseWriter, r *http.Request) {
					require.NoError(t, json.NewDecoder(r.Body).Decode(&gotBody))
					w.WriteHeader(http.StatusOK)
					_, _ = w.Write(MustMarshal(updatedGist))
				},
			}))
			deps := BaseDeps{Client: client}
			handler := serverTool.Handler(deps)
			request := createMCPRequest(tc.args)

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

			desc, present := gotBody["description"]
			assert.Equal(t, tc.wantDescPresent, present, "description key presence in PATCH body")
			if tc.wantDescPresent {
				assert.Equal(t, tc.wantDescValue, desc)
			}
		})
	}
}
