package github

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

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

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

	assert.Equal(t, "list_notifications", 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, "filter")
	assert.Contains(t, schema.Properties, "since")
	assert.Contains(t, schema.Properties, "before")
	assert.Contains(t, schema.Properties, "owner")
	assert.Contains(t, schema.Properties, "repo")
	assert.Contains(t, schema.Properties, "page")
	assert.Contains(t, schema.Properties, "perPage")
	// All fields are optional, so Required should be empty
	assert.Empty(t, schema.Required)
	mockNotification := &github.Notification{
		ID:     github.Ptr("123"),
		Reason: github.Ptr("mention"),
	}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectedResult []*github.Notification
		expectedErrMsg string
	}{
		{
			name: "success default filter (no params)",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetNotifications: mockResponse(t, http.StatusOK, []*github.Notification{mockNotification}),
			}),
			requestArgs:    map[string]any{},
			expectError:    false,
			expectedResult: []*github.Notification{mockNotification},
		},
		{
			name: "success with filter=include_read_notifications",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetNotifications: mockResponse(t, http.StatusOK, []*github.Notification{mockNotification}),
			}),
			requestArgs: map[string]any{
				"filter": "include_read_notifications",
			},
			expectError:    false,
			expectedResult: []*github.Notification{mockNotification},
		},
		{
			name: "success with filter=only_participating",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetNotifications: mockResponse(t, http.StatusOK, []*github.Notification{mockNotification}),
			}),
			requestArgs: map[string]any{
				"filter": "only_participating",
			},
			expectError:    false,
			expectedResult: []*github.Notification{mockNotification},
		},
		{
			name: "success for repo notifications",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetReposNotificationsByOwnerByRepo: mockResponse(t, http.StatusOK, []*github.Notification{mockNotification}),
			}),
			requestArgs: map[string]any{
				"filter":  "default",
				"since":   "2024-01-01T00:00:00Z",
				"before":  "2024-01-02T00:00:00Z",
				"owner":   "octocat",
				"repo":    "hello-world",
				"page":    float64(2),
				"perPage": float64(10),
			},
			expectError:    false,
			expectedResult: []*github.Notification{mockNotification},
		},
		{
			name: "error",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetNotifications: mockResponse(t, http.StatusInternalServerError, `{"message": "error"}`),
			}),
			requestArgs:    map[string]any{},
			expectError:    true,
			expectedErrMsg: "error",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := github.NewClient(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.expectError {
				require.True(t, result.IsError)
				errorContent := getErrorResult(t, result)
				if tc.expectedErrMsg != "" {
					assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
				}
				return
			}

			require.False(t, result.IsError)
			textContent := getTextResult(t, result)
			t.Logf("textContent: %s", textContent.Text)
			var returned []*github.Notification
			err = json.Unmarshal([]byte(textContent.Text), &returned)
			require.NoError(t, err)
			require.NotEmpty(t, returned)
			assert.Equal(t, *tc.expectedResult[0].ID, *returned[0].ID)
		})
	}
}

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

	assert.Equal(t, "manage_notification_subscription", 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, "notificationID")
	assert.Contains(t, schema.Properties, "action")
	assert.Equal(t, []string{"notificationID", "action"}, schema.Required)

	mockSub := &github.Subscription{Ignored: github.Ptr(true)}
	mockSubWatch := &github.Subscription{Ignored: github.Ptr(false), Subscribed: github.Ptr(true)}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectIgnored  *bool
		expectDeleted  bool
		expectInvalid  bool
		expectedErrMsg string
	}{
		{
			name: "ignore subscription",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PutNotificationsThreadsSubscriptionByThreadID: mockResponse(t, http.StatusOK, mockSub),
			}),
			requestArgs: map[string]any{
				"notificationID": "123",
				"action":         "ignore",
			},
			expectError:   false,
			expectIgnored: github.Ptr(true),
		},
		{
			name: "watch subscription",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PutNotificationsThreadsSubscriptionByThreadID: mockResponse(t, http.StatusOK, mockSubWatch),
			}),
			requestArgs: map[string]any{
				"notificationID": "123",
				"action":         "watch",
			},
			expectError:   false,
			expectIgnored: github.Ptr(false),
		},
		{
			name: "delete subscription",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				DeleteNotificationsThreadsSubscriptionByThreadID: mockResponse(t, http.StatusOK, nil),
			}),
			requestArgs: map[string]any{
				"notificationID": "123",
				"action":         "delete",
			},
			expectError:   false,
			expectDeleted: true,
		},
		{
			name:         "invalid action",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"notificationID": "123",
				"action":         "invalid",
			},
			expectError:   false,
			expectInvalid: true,
		},
		{
			name:         "missing required notificationID",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"action": "ignore",
			},
			expectError: true,
		},
		{
			name:         "missing required action",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"notificationID": "123",
			},
			expectError: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := github.NewClient(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.expectError {
				require.NoError(t, err)
				require.NotNil(t, result)
				text := getTextResult(t, result).Text
				switch {
				case tc.requestArgs["notificationID"] == nil:
					assert.Contains(t, text, "missing required parameter: notificationID")
				case tc.requestArgs["action"] == nil:
					assert.Contains(t, text, "missing required parameter: action")
				default:
					assert.Contains(t, text, "error")
				}
				return
			}

			require.NoError(t, err)
			textContent := getTextResult(t, result)
			if tc.expectIgnored != nil {
				var returned github.Subscription
				err = json.Unmarshal([]byte(textContent.Text), &returned)
				require.NoError(t, err)
				assert.Equal(t, *tc.expectIgnored, *returned.Ignored)
			}
			if tc.expectDeleted {
				assert.Contains(t, textContent.Text, "deleted")
			}
			if tc.expectInvalid {
				assert.Contains(t, textContent.Text, "Invalid action")
			}
		})
	}
}

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

	assert.Equal(t, "manage_repository_notification_subscription", 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, "owner")
	assert.Contains(t, schema.Properties, "repo")
	assert.Contains(t, schema.Properties, "action")
	assert.Equal(t, []string{"owner", "repo", "action"}, schema.Required)

	mockSub := &github.Subscription{Ignored: github.Ptr(true)}
	mockWatchSub := &github.Subscription{Ignored: github.Ptr(false), Subscribed: github.Ptr(true)}

	tests := []struct {
		name             string
		mockedClient     *http.Client
		requestArgs      map[string]any
		expectError      bool
		expectIgnored    *bool
		expectSubscribed *bool
		expectDeleted    bool
		expectInvalid    bool
		expectedErrMsg   string
	}{
		{
			name: "ignore subscription",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PutReposSubscriptionByOwnerByRepo: mockResponse(t, http.StatusOK, mockSub),
			}),
			requestArgs: map[string]any{
				"owner":  "owner",
				"repo":   "repo",
				"action": "ignore",
			},
			expectError:   false,
			expectIgnored: github.Ptr(true),
		},
		{
			name: "watch subscription",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PutReposSubscriptionByOwnerByRepo: mockResponse(t, http.StatusOK, mockWatchSub),
			}),
			requestArgs: map[string]any{
				"owner":  "owner",
				"repo":   "repo",
				"action": "watch",
			},
			expectError:      false,
			expectIgnored:    github.Ptr(false),
			expectSubscribed: github.Ptr(true),
		},
		{
			name: "delete subscription",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				DeleteReposSubscriptionByOwnerByRepo: mockResponse(t, http.StatusOK, nil),
			}),
			requestArgs: map[string]any{
				"owner":  "owner",
				"repo":   "repo",
				"action": "delete",
			},
			expectError:   false,
			expectDeleted: true,
		},
		{
			name:         "invalid action",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"owner":  "owner",
				"repo":   "repo",
				"action": "invalid",
			},
			expectError:   false,
			expectInvalid: true,
		},
		{
			name:         "missing required owner",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"repo":   "repo",
				"action": "ignore",
			},
			expectError: true,
		},
		{
			name:         "missing required repo",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"owner":  "owner",
				"action": "ignore",
			},
			expectError: true,
		},
		{
			name:         "missing required action",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"owner": "owner",
				"repo":  "repo",
			},
			expectError: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := github.NewClient(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.expectError {
				require.NotNil(t, result)
				text := getTextResult(t, result).Text
				switch {
				case tc.requestArgs["owner"] == nil:
					assert.Contains(t, text, "missing required parameter: owner")
				case tc.requestArgs["repo"] == nil:
					assert.Contains(t, text, "missing required parameter: repo")
				case tc.requestArgs["action"] == nil:
					assert.Contains(t, text, "missing required parameter: action")
				default:
					assert.Contains(t, text, "error")
				}
				return
			}

			require.NoError(t, err)
			textContent := getTextResult(t, result)
			if tc.expectIgnored != nil || tc.expectSubscribed != nil {
				var returned github.Subscription
				err = json.Unmarshal([]byte(textContent.Text), &returned)
				require.NoError(t, err)
				if tc.expectIgnored != nil {
					assert.Equal(t, *tc.expectIgnored, *returned.Ignored)
				}
				if tc.expectSubscribed != nil {
					assert.Equal(t, *tc.expectSubscribed, *returned.Subscribed)
				}
			}
			if tc.expectDeleted {
				assert.Contains(t, textContent.Text, "deleted")
			}
			if tc.expectInvalid {
				assert.Contains(t, textContent.Text, "Invalid action")
			}
		})
	}
}

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

	assert.Equal(t, "dismiss_notification", 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, "threadID")
	assert.Contains(t, schema.Properties, "state")
	assert.Equal(t, []string{"threadID", "state"}, schema.Required)

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectRead     bool
		expectDone     bool
		expectInvalid  bool
		expectedErrMsg string
	}{
		{
			name: "mark as read",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PatchNotificationsThreadsByThreadID: mockResponse(t, http.StatusOK, nil),
			}),
			requestArgs: map[string]any{
				"threadID": "123",
				"state":    "read",
			},
			expectError: false,
			expectRead:  true,
		},
		{
			name: "mark as done with 204 response",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				DeleteNotificationsThreadsByThreadID: mockResponse(t, http.StatusNoContent, nil),
			}),
			requestArgs: map[string]any{
				"threadID": "123",
				"state":    "done",
			},
			expectError: false,
			expectDone:  true,
		},
		{
			name: "mark as done with 200 response",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				DeleteNotificationsThreadsByThreadID: mockResponse(t, http.StatusOK, nil),
			}),
			requestArgs: map[string]any{
				"threadID": "123",
				"state":    "done",
			},
			expectError: false,
			expectDone:  true,
		},
		{
			name:         "invalid threadID format",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"threadID": "notanumber",
				"state":    "done",
			},
			expectError:   false,
			expectInvalid: true,
		},
		{
			name:         "missing required threadID",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"state": "read",
			},
			expectError: true,
		},
		{
			name:         "missing required state",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"threadID": "123",
			},
			expectError: true,
		},
		{
			name:         "invalid state value",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
			requestArgs: map[string]any{
				"threadID": "123",
				"state":    "invalid",
			},
			expectError: true,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := github.NewClient(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.expectError {
				// The tool returns a ToolResultError with a specific message
				require.NotNil(t, result)
				text := getTextResult(t, result).Text
				switch {
				case tc.requestArgs["threadID"] == nil:
					assert.Contains(t, text, "missing required parameter: threadID")
				case tc.requestArgs["state"] == nil:
					assert.Contains(t, text, "missing required parameter: state")
				case tc.name == "invalid threadID format":
					assert.Contains(t, text, "invalid threadID format")
				case tc.name == "invalid state value":
					assert.Contains(t, text, "Invalid state. Must be one of: read, done.")
				default:
					// fallback for other errors
					assert.Contains(t, text, "error")
				}
				return
			}

			require.NoError(t, err)
			textContent := getTextResult(t, result)
			if tc.expectRead {
				assert.Contains(t, textContent.Text, "Notification marked as read")
			}
			if tc.expectDone {
				assert.Contains(t, textContent.Text, "Notification marked as done")
			}
			if tc.expectInvalid {
				assert.Contains(t, textContent.Text, "invalid threadID format")
			}
		})
	}
}

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

	assert.Equal(t, "mark_all_notifications_read", 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, "lastReadAt")
	assert.Contains(t, schema.Properties, "owner")
	assert.Contains(t, schema.Properties, "repo")
	assert.Empty(t, schema.Required)

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectMarked   bool
		expectedErrMsg string
	}{
		{
			name: "success (no params)",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PutNotifications: mockResponse(t, http.StatusOK, nil),
			}),
			requestArgs:  map[string]any{},
			expectError:  false,
			expectMarked: true,
		},
		{
			name: "success with lastReadAt param",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PutNotifications: mockResponse(t, http.StatusOK, nil),
			}),
			requestArgs: map[string]any{
				"lastReadAt": "2024-01-01T00:00:00Z",
			},
			expectError:  false,
			expectMarked: true,
		},
		{
			name: "success with owner and repo",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PutReposNotificationsByOwnerByRepo: mockResponse(t, http.StatusOK, nil),
			}),
			requestArgs: map[string]any{
				"owner": "octocat",
				"repo":  "hello-world",
			},
			expectError:  false,
			expectMarked: true,
		},
		{
			name: "API error",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				PutNotifications: mockResponse(t, http.StatusInternalServerError, `{"message": "error"}`),
			}),
			requestArgs:    map[string]any{},
			expectError:    true,
			expectedErrMsg: "error",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := github.NewClient(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.expectError {
				require.True(t, result.IsError)
				errorContent := getErrorResult(t, result)
				if tc.expectedErrMsg != "" {
					assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
				}
				return
			}

			require.NoError(t, err)
			require.False(t, result.IsError)
			textContent := getTextResult(t, result)
			if tc.expectMarked {
				assert.Contains(t, textContent.Text, "All notifications marked as read")
			}
		})
	}
}

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

	assert.Equal(t, "get_notification_details", 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, "notificationID")
	assert.Equal(t, []string{"notificationID"}, schema.Required)

	mockThread := &github.Notification{ID: github.Ptr("123"), Reason: github.Ptr("mention")}

	tests := []struct {
		name           string
		mockedClient   *http.Client
		requestArgs    map[string]any
		expectError    bool
		expectResult   *github.Notification
		expectedErrMsg string
	}{
		{
			name: "success",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetNotificationsThreadsByThreadID: mockResponse(t, http.StatusOK, mockThread),
			}),
			requestArgs: map[string]any{
				"notificationID": "123",
			},
			expectError:  false,
			expectResult: mockThread,
		},
		{
			name: "not found",
			mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
				GetNotificationsThreadsByThreadID: mockResponse(t, http.StatusNotFound, `{"message": "not found"}`),
			}),
			requestArgs: map[string]any{
				"notificationID": "123",
			},
			expectError:    true,
			expectedErrMsg: "not found",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			client := github.NewClient(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.expectError {
				require.True(t, result.IsError)
				errorContent := getErrorResult(t, result)
				if tc.expectedErrMsg != "" {
					assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
				}
				return
			}

			require.NoError(t, err)
			require.False(t, result.IsError)
			textContent := getTextResult(t, result)
			var returned github.Notification
			err = json.Unmarshal([]byte(textContent.Text), &returned)
			require.NoError(t, err)
			assert.Equal(t, *tc.expectResult.ID, *returned.ID)
		})
	}
}
