package provider

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/store/app"
)

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

	proposalState := githubFlowState{Flow: "proposal_install", ProposalID: "proposal-1"}
	tests := []struct {
		name          string
		installError  error
		expectedState githubFlowState
		expectedError error
		expectedFlows []string
	}{
		{
			name:          "install flow succeeds",
			expectedState: githubFlowState{Flow: "install"},
			expectedFlows: []string{"install"},
		},
		{
			name:          "cookie is absent",
			installError:  http.ErrNoCookie,
			expectedState: githubFlowState{Flow: "install"},
			expectedError: http.ErrNoCookie,
			expectedFlows: []string{"install"},
		},
		{
			name:          "proposal flow shares the install cookie",
			installError:  errors.New("install flow mismatch"),
			expectedState: proposalState,
			expectedFlows: []string{"install", "proposal_install"},
		},
	}

	for _, testCase := range tests {
		testCase := testCase
		t.Run(testCase.name, func(t *testing.T) {
			t.Parallel()
			flows := make([]string, 0, 2)
			state, err := consumeGitHubInstallFlowCookie(func(expectedFlow string) (githubFlowState, error) {
				flows = append(flows, expectedFlow)
				if expectedFlow == "install" {
					return githubFlowState{Flow: "install"}, testCase.installError
				}
				return proposalState, nil
			})

			require.Equal(t, testCase.expectedState, state)
			require.ErrorIs(t, err, testCase.expectedError)
			require.Equal(t, testCase.expectedFlows, flows)
		})
	}
}

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

	handler := &GitHubHandler{}
	state, err := handler.newGitHubFlowState("register", "usr_member", "wsp_1", app.GitHubAppSetupModeRequester)
	require.NoError(t, err)
	require.Equal(t, githubFlowStateVersion, state.Version)
	require.Equal(t, app.GitHubAppSetupModeRequester, state.Mode)
	require.Equal(t, "usr_member", state.UserID)
	require.Equal(t, "wsp_1", state.WorkspaceID)

	installState, err := handler.newGitHubFlowState("install", "usr_member", "wsp_1", app.GitHubAppSetupModeRequester)
	require.NoError(t, err)
	require.Equal(t, app.GitHubAppSetupModeRequester, installState.Mode)
}

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

	handler := &GitHubHandler{
		githubConfig:  GitHubConfig{FlowStateKey: []byte("0123456789abcdef0123456789abcdef")},
		publicBaseURL: "http://agentteam.example.com",
	}
	legacy := githubFlowState{
		Version:     "v1",
		Flow:        "register",
		Nonce:       "legacy-nonce",
		UserID:      "usr_member",
		WorkspaceID: "wsp_1",
		ExpiresAt:   time.Now().UTC().Add(time.Hour).Unix(),
	}
	issueRecorder := httptest.NewRecorder()
	issueContext, _ := gin.CreateTestContext(issueRecorder)
	require.NoError(t, handler.issueGitHubFlowCookie(issueContext, githubRegisterCookieName, legacy))
	cookies := issueRecorder.Result().Cookies()
	require.Len(t, cookies, 1)

	request := httptest.NewRequest(http.MethodGet, "/api/github/callback", nil)
	request.AddCookie(cookies[0])
	consumeContext, _ := gin.CreateTestContext(httptest.NewRecorder())
	consumeContext.Request = request
	_, err := handler.consumeGitHubFlowCookie(consumeContext, githubRegisterCookieName, "register", legacy.Nonce)
	require.ErrorContains(t, err, "unsupported github flow cookie version")
}

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

	handler := &GitHubHandler{
		publicBaseURL: "https://agentteam.example.com",
	}

	manifestJSON, err := handler.buildGitHubManifestJSON()
	require.NoError(t, err)

	var manifest map[string]any
	require.NoError(t, json.Unmarshal([]byte(manifestJSON), &manifest))

	permissions, ok := manifest["default_permissions"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "read", permissions["pull_requests"])
	require.Equal(t, "read", permissions["issues"])
	require.Equal(t, "read", permissions["metadata"])
	_, hasInvalidCommitStatuses := permissions["commit_statuses"]
	require.False(t, hasInvalidCommitStatuses)
	_, hasChecks := permissions["checks"]
	require.False(t, hasChecks)
	_, hasStatuses := permissions["statuses"]
	require.False(t, hasStatuses)

	defaultEvents, ok := manifest["default_events"].([]any)
	require.True(t, ok)

	events := make([]string, 0, len(defaultEvents))
	for _, value := range defaultEvents {
		text, ok := value.(string)
		require.True(t, ok)
		events = append(events, text)
	}

	require.Contains(t, events, "pull_request")
	require.Contains(t, events, "pull_request_review")
	require.Contains(t, events, "pull_request_review_comment")
	require.Contains(t, events, "issue_comment")
	require.NotContains(t, events, "pull_request_review_thread")
	require.NotContains(t, events, "check_run")
	require.NotContains(t, events, "check_suite")
	require.NotContains(t, events, "workflow_run")
	require.NotContains(t, events, "discussion_comment")
}

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

	installation := githubInstallationLookupResponse{
		Permissions: map[string]string{
			"pull_requests": "read",
			"metadata":      "read",
		},
		Events: []string{
			"pull_request",
			"pull_request_review",
		},
	}

	status := githubInstallationUpgradeRequirements(installation)
	require.Contains(t, status.MissingPermissions, "issues")
	require.Contains(t, status.MissingEvents, "pull_request_review_comment")
	require.Contains(t, status.MissingEvents, "issue_comment")
	require.Len(t, status.MissingPermissions, 1)
	require.Len(t, status.MissingEvents, 2)
}

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

	installation := githubInstallationLookupResponse{
		Permissions: githubManifestPermissions(),
		Events:      githubManifestEvents(),
	}

	status := githubInstallationUpgradeRequirements(installation)
	require.Empty(t, status.MissingPermissions)
	require.Empty(t, status.MissingEvents)
}

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

	valid := githubManifestConversionResponse{
		ID:            42,
		Slug:          "agentteam-app",
		Name:          "AgentTeam App",
		ClientID:      "Iv1.client",
		ClientSecret:  "client-secret",
		PEM:           "private-key",
		WebhookSecret: "webhook-secret",
		HTMLURL:       "https://github.com/apps/agentteam-app",
	}
	valid.Owner.ID = 7
	valid.Owner.Login = "acme"
	require.NoError(t, validateGitHubManifestConversion(valid))

	tests := map[string]func(*githubManifestConversionResponse){
		"app id":         func(value *githubManifestConversionResponse) { value.ID = 0 },
		"slug":           func(value *githubManifestConversionResponse) { value.Slug = "" },
		"name":           func(value *githubManifestConversionResponse) { value.Name = "" },
		"client id":      func(value *githubManifestConversionResponse) { value.ClientID = "" },
		"client secret":  func(value *githubManifestConversionResponse) { value.ClientSecret = "" },
		"private key":    func(value *githubManifestConversionResponse) { value.PEM = "" },
		"webhook secret": func(value *githubManifestConversionResponse) { value.WebhookSecret = "" },
		"html url":       func(value *githubManifestConversionResponse) { value.HTMLURL = "" },
		"owner id":       func(value *githubManifestConversionResponse) { value.Owner.ID = 0 },
		"owner login":    func(value *githubManifestConversionResponse) { value.Owner.Login = "" },
	}
	for name, mutate := range tests {
		name, mutate := name, mutate
		t.Run(name, func(t *testing.T) {
			t.Parallel()
			candidate := valid
			mutate(&candidate)
			require.ErrorContains(t, validateGitHubManifestConversion(candidate), "incomplete")
		})
	}
}
