package provider_test

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"encoding/json"
	"encoding/pem"
	"net/http"
	"net/http/httptest"
	"net/url"
	"slices"
	"strings"
	"sync"
	"testing"
	"time"

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

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	. "github.com/tidbcloud/AgentTeam/internal/api/testkit"
	"github.com/tidbcloud/AgentTeam/internal/store/provider"
)

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

	privateKeyPEM := testGitHubPrivateKeyPEM(t)
	github := newGitHubFlowTestServer(t, privateKeyPEM)
	defer github.Close()

	runtime := StartRuntimeWithGitHubEndpoints(t, github.URL(), github.URL())

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()
	admin := RegisterUserForOrganizationTests(t, client, server.URL, "github-config-admin@example.com", "github-config-admin")

	var workspaces []dto.WorkspaceResponse
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(admin.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	installations, err := runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, admin.User.ID, workspaceID)
	require.NoError(t, err)
	require.Empty(t, installations)

	var initial provider.GitHubAppConfigView
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/config", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &initial)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.False(t, initial.Registered)

	var draft dto.GitHubSetupDraftResponse
	response = DoJSON(t, client, http.MethodPut, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/setup-draft", map[string]any{
		"settings_url":    github.URL() + "/organizations/acme/settings/apps/agentteam-http",
		"private_key_pem": privateKeyPEM,
	}, WorkspaceHeaders(admin.AccessToken, workspaceID), &draft)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.True(t, draft.Proposal.Ready)
	require.NotEmpty(t, draft.Proposal.ID)
	require.NotEmpty(t, draft.Installations)
	require.Equal(t, "789", draft.Installations[0].InstallationID)

	var installed dto.ProviderAppInstallationResponse
	response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/setup-draft/complete", map[string]any{
		"setup_proposal_id": draft.Proposal.ID,
		// Exercise the second eligible candidate; provider unit coverage separately
		// proves exact-GET permission drift is rejected before webhook mutation.
		"installation_id": "790",
	}, WorkspaceHeaders(admin.AccessToken, workspaceID), &installed)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.True(t, installed.Enabled)
	require.Len(t, installed.Connections, 1)
	require.Equal(t, "790", installed.Connections[0].ExternalSubjectRef)
	require.True(t, installed.Connections[0].Enabled)

	var registered provider.GitHubAppConfigView
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/config", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &registered)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.True(t, registered.Registered)
	require.Equal(t, int64(4242), registered.AppID)
	require.Equal(t, "agentteam-http", registered.AppSlug)
	require.Equal(t, "AgentTeam HTTP", registered.AppName)
	require.Equal(t, "https://github.com/apps/agentteam-http", registered.HTMLURL)
	require.Equal(t, "acme", registered.OwnerLogin)

	config, found, err := runtime.Store.App().Provider().LoadGitHubAppConfigForWorkspace(runtime.Ctx, workspaceID)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, int64(4242), config.AppID)
	require.Equal(t, "agentteam-http", config.AppSlug)
	require.Equal(t, "AgentTeam HTTP", config.AppName)
	require.Equal(t, "Iv1.http", config.ClientID)
	require.Equal(t, "", config.ClientSecret)
	require.Equal(t, strings.TrimSpace(privateKeyPEM), config.PrivateKeyPEM)
	require.NotEmpty(t, config.WebhookSecret)
	require.Equal(t, "acme", config.OwnerLogin)

	webhookUpdates, webhookURL, webhookSecret, contentType := github.webhookConfigState()
	require.Equal(t, 1, webhookUpdates)
	require.Equal(t, "http://localhost:3001/api/apps/github/webhook", webhookURL)
	require.Equal(t, config.WebhookSecret, webhookSecret)
	require.Equal(t, "json", contentType)

	installations, err = runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, admin.User.ID, workspaceID)
	require.NoError(t, err)
	require.Len(t, installations, 1)
	require.Equal(t, "github", installations[0].AppSlug)
	require.Len(t, installations[0].Connections, 1, "fresh setup must carry the selected connection set")

	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/config", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &registered)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.True(t, registered.Registered)
	require.Equal(t, int64(4242), registered.AppID)
	require.Equal(t, "agentteam-http", registered.AppSlug)
	require.Equal(t, "AgentTeam HTTP", registered.AppName)
	require.Equal(t, "https://github.com/apps/agentteam-http", registered.HTMLURL)
	require.Equal(t, "acme", registered.OwnerLogin)

	var deleted provider.GitHubAppConfigView
	response = DoJSON(t, client, http.MethodDelete, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/config", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &deleted)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.False(t, deleted.Registered)

	var afterDelete provider.GitHubAppConfigView
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/config", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &afterDelete)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.False(t, afterDelete.Registered)
}

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

	privateKeyPEM := testGitHubPrivateKeyPEM(t)
	github := newGitHubFlowTestServer(t, privateKeyPEM)
	defer github.Close()

	runtime := StartRuntimeWithGitHubEndpoints(t, github.URL(), github.URL())

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()
	admin := RegisterUserForOrganizationTests(t, client, server.URL, "github-register-admin@example.com", "github-register-admin")

	var workspaces []dto.WorkspaceResponse
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(admin.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	var started struct {
		FormAction string `json:"formAction"`
		Manifest   string `json:"manifest"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/registration", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &started)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Contains(t, started.FormAction, github.URL()+"/settings/apps/new?state=")
	require.NotContains(t, started.FormAction, "/organizations/")
	require.NotEmpty(t, response.Header.Get("Set-Cookie"))

	installations, err := runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, admin.User.ID, workspaceID)
	require.NoError(t, err)
	require.Empty(t, installations)
	configBeforeCallback, foundBeforeCallback, err := runtime.Store.App().Provider().LoadGitHubAppConfigForWorkspace(runtime.Ctx, workspaceID)
	require.NoError(t, err)
	require.False(t, foundBeforeCallback)
	require.Equal(t, provider.GitHubAppConfig{}, configBeforeCallback)

	var manifest map[string]any
	require.NoError(t, json.Unmarshal([]byte(started.Manifest), &manifest))
	require.Equal(t, "http://localhost:3001/api/apps/github/webhook", manifest["hook_attributes"].(map[string]any)["url"])
	require.Equal(t, "http://localhost:3001/api/github/callback", manifest["redirect_url"])
	require.Equal(t, "http://localhost:3001/api/github/setup", manifest["setup_url"])
	require.Equal(t, true, manifest["public"])

	formActionURL, err := url.Parse(started.FormAction)
	require.NoError(t, err)
	stateValue := formActionURL.Query().Get("state")
	require.NotEmpty(t, stateValue)

	redirectClient := noRedirectHTTPClient(client)
	request, err := http.NewRequest(http.MethodGet, server.URL+"/api/github/callback?code=test-manifest-code&state="+url.QueryEscape(stateValue), nil)
	require.NoError(t, err)
	request.Header.Set("Cookie", response.Header.Get("Set-Cookie"))
	callbackResponse, err := redirectClient.Do(request)
	require.NoError(t, err)
	t.Cleanup(func() { _ = callbackResponse.Body.Close() })
	require.Equal(t, http.StatusFound, callbackResponse.StatusCode)
	require.Equal(t, "/apps/github?github_app=registered", callbackResponse.Header.Get("Location"))

	config, found, err := runtime.Store.App().Provider().LoadGitHubAppConfigForWorkspace(runtime.Ctx, workspaceID)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, int64(4242), config.AppID)
	require.Equal(t, "agentteam-http", config.AppSlug)
	require.Equal(t, "AgentTeam HTTP", config.AppName)
	require.Equal(t, "Iv1.http", config.ClientID)
	require.Equal(t, "client-secret", config.ClientSecret)
	require.Equal(t, strings.TrimSpace(privateKeyPEM), config.PrivateKeyPEM)
	require.Equal(t, "webhook-secret", config.WebhookSecret)
	require.Equal(t, "acme", config.OwnerLogin)

	installations, err = runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, admin.User.ID, workspaceID)
	require.NoError(t, err)
	require.Len(t, installations, 1)
	require.Equal(t, "github", installations[0].AppSlug)
}

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

	privateKeyPEM := testGitHubPrivateKeyPEM(t)
	github := newGitHubFlowTestServer(t, privateKeyPEM)
	defer github.Close()

	runtime := StartRuntimeWithGitHubEndpoints(t, github.URL(), github.URL())

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()
	admin := RegisterUserForOrganizationTests(t, client, server.URL, "github-member-setup-admin@example.com", "github-member-setup-admin")
	member := RegisterUserForOrganizationTests(t, client, server.URL, "github-member-setup-member@example.com", "github-member-setup-member")

	var workspaces []dto.WorkspaceResponse
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(admin.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	now := time.Now().UTC()
	const memberID = "mem_github_setup_requester"
	_, err := runtime.Pool.Exec(runtime.Ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES ($1, $2, 'human', 'GitHub Setup Requester', $3, 'member', 'active', $4, $4)
`, memberID, workspaceID, member.User.ID, now)
	require.NoError(t, err)

	var started struct {
		FormAction string `json:"formAction"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/registration", nil, WorkspaceHeaders(member.AccessToken, workspaceID), &started)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, response.Header.Get("Set-Cookie"))
	formActionURL, err := url.Parse(started.FormAction)
	require.NoError(t, err)
	stateValue := formActionURL.Query().Get("state")
	require.NotEmpty(t, stateValue)

	redirectClient := noRedirectHTTPClient(client)
	request, err := http.NewRequest(http.MethodGet, server.URL+"/api/github/callback?code=test-manifest-code&state="+url.QueryEscape(stateValue), nil)
	require.NoError(t, err)
	request.Header.Set("Cookie", response.Header.Get("Set-Cookie"))
	callbackResponse, err := redirectClient.Do(request)
	require.NoError(t, err)
	t.Cleanup(func() { _ = callbackResponse.Body.Close() })
	require.Equal(t, http.StatusFound, callbackResponse.StatusCode)
	require.Equal(t, "/apps/github?github_app=ready", callbackResponse.Header.Get("Location"))

	config, found, err := runtime.Store.App().Provider().LoadGitHubAppConfigForWorkspace(runtime.Ctx, workspaceID)
	require.NoError(t, err)
	require.False(t, found)
	require.Equal(t, provider.GitHubAppConfig{}, config)
	installations, err := runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, member.User.ID, workspaceID)
	require.NoError(t, err)
	require.Empty(t, installations)

	var proposalEnvelope dto.GitHubSetupProposalEnvelope
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/setup-proposal", nil, WorkspaceHeaders(member.AccessToken, workspaceID), &proposalEnvelope)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotNil(t, proposalEnvelope.Proposal)
	require.True(t, proposalEnvelope.Proposal.Ready)
	require.Equal(t, "4242", proposalEnvelope.Proposal.AppID)
	require.Equal(t, "agentteam-http", proposalEnvelope.Proposal.AppSlug)
	require.Equal(t, "AgentTeam HTTP", proposalEnvelope.Proposal.AppName)
	require.Equal(t, "acme", proposalEnvelope.Proposal.OwnerLogin)

	var rawDraft, draftStatus string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT requested_config, status
FROM service_installation_requests
WHERE id = $1 AND workspace_id = $2
`, proposalEnvelope.Proposal.ID, workspaceID).Scan(&rawDraft, &draftStatus))
	require.Equal(t, "setup_ready", draftStatus)
	require.NotContains(t, rawDraft, "client-secret")
	require.NotContains(t, rawDraft, "webhook-secret")
	require.NotContains(t, rawDraft, strings.TrimSpace(privateKeyPEM))
	require.Contains(t, rawDraft, "ciphertext")

	var proposalInstall struct {
		InstallURL string `json:"installUrl"`
	}
	response = DoJSON(t, client, http.MethodGet,
		server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/setup-proposal/"+proposalEnvelope.Proposal.ID+"/installation",
		nil, WorkspaceHeaders(member.AccessToken, workspaceID), &proposalInstall)
	require.Equal(t, http.StatusOK, response.StatusCode)
	parsedInstallURL, err := url.Parse(proposalInstall.InstallURL)
	require.NoError(t, err)
	require.NotEmpty(t, parsedInstallURL.Query().Get("state"))

	proposalCallback, err := http.NewRequest(
		http.MethodGet,
		server.URL+"/api/github/setup?installation_id=789&setup_action=install&state="+
			url.QueryEscape(parsedInstallURL.Query().Get("state")),
		nil,
	)
	require.NoError(t, err)
	proposalCallback.Header.Set("Cookie", response.Header.Get("Set-Cookie"))
	proposalCallbackResponse, err := noRedirectHTTPClient(client).Do(proposalCallback)
	require.NoError(t, err)
	t.Cleanup(func() { _ = proposalCallbackResponse.Body.Close() })
	require.Equal(t, http.StatusFound, proposalCallbackResponse.StatusCode)
	require.Equal(t, "/apps/github?github_app=ready", proposalCallbackResponse.Header.Get("Location"))

	var discovered []dto.GitHubInstallationResponse
	response = DoJSON(t, client, http.MethodGet,
		server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/setup-proposal/"+proposalEnvelope.Proposal.ID+"/installations",
		nil, WorkspaceHeaders(member.AccessToken, workspaceID), &discovered)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, discovered)
	require.Equal(t, "789", discovered[0].InstallationID)

	requestPath := server.URL + "/api/v1/workspaces/" + workspaceID + "/app-installation-requests"
	var installRequest dto.AppInstallationRequestResponse
	response = DoJSON(t, client, http.MethodPost, requestPath, map[string]any{
		"appId":                 "app_github",
		"appSlug":               "github",
		"requestKind":           "install",
		"githubSetupProposalId": proposalEnvelope.Proposal.ID,
		"connectionTarget":      "789",
		"reason":                "Enable GitHub for this workspace",
	}, WorkspaceHeaders(member.AccessToken, workspaceID), &installRequest)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, proposalEnvelope.Proposal.ID, installRequest.ID)
	require.Equal(t, "install", installRequest.RequestKind)
	require.Equal(t, "pending", installRequest.Status)

	config, found, err = runtime.Store.App().Provider().LoadGitHubAppConfigForWorkspace(runtime.Ctx, workspaceID)
	require.NoError(t, err)
	require.False(t, found)
	installations, err = runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, member.User.ID, workspaceID)
	require.NoError(t, err)
	require.Empty(t, installations)

	var resolvedConfig string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT requested_config
FROM service_installation_requests
WHERE id = $1 AND workspace_id = $2
`, installRequest.ID, workspaceID).Scan(&resolvedConfig))
	require.Contains(t, resolvedConfig, "ciphertext", "pending request retains only encrypted credentials until approval")
	require.NotContains(t, resolvedConfig, "client-secret")
	require.NotContains(t, resolvedConfig, "webhook-secret")
	require.NotContains(t, resolvedConfig, strings.TrimSpace(privateKeyPEM))
}

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

	privateKeyPEM := testGitHubPrivateKeyPEM(t)
	github := newGitHubFlowTestServer(t, privateKeyPEM)
	defer github.Close()

	runtime := StartRuntimeWithGitHubEndpoints(t, github.URL(), github.URL())

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()
	admin := RegisterUserForOrganizationTests(t, client, server.URL, "github-install-admin@example.com", "github-install-admin")

	var workspaces []dto.WorkspaceResponse
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(admin.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	requestAndApproveProviderAppForTest(t, runtime, admin.User.ID, workspaceID, "github")

	_, err := runtime.Store.App().Provider().SaveGitHubAppConfigForWorkspaceAdmin(runtime.Ctx, admin.User.ID, workspaceID, provider.UpsertGitHubAppConfigParams{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		AppName:       "AgentTeam HTTP",
		HTMLURL:       "https://github.com/apps/agentteam-http",
		ClientID:      "Iv1.http",
		ClientSecret:  "client-secret",
		PrivateKeyPEM: privateKeyPEM,
		WebhookSecret: "webhook-secret",
		OwnerLogin:    "acme",
		OwnerID:       7,
	})
	require.NoError(t, err)

	startInstall := func() (string, string) {
		var started struct {
			InstallURL string `json:"installUrl"`
		}
		resp := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/installation", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &started)
		require.Equal(t, http.StatusOK, resp.StatusCode)
		require.NotEmpty(t, started.InstallURL)
		parsed, err := url.Parse(started.InstallURL)
		require.NoError(t, err)
		return parsed.Query().Get("state"), resp.Header.Get("Set-Cookie")
	}

	redirectClient := noRedirectHTTPClient(client)

	stateInstall, cookieInstall := startInstall()
	request, err := http.NewRequest(http.MethodGet, server.URL+"/api/github/setup?installation_id=789&setup_action=install&state="+url.QueryEscape(stateInstall), nil)
	require.NoError(t, err)
	request.Header.Set("Cookie", cookieInstall)
	setupResponse, err := redirectClient.Do(request)
	require.NoError(t, err)
	t.Cleanup(func() { _ = setupResponse.Body.Close() })
	require.Equal(t, http.StatusFound, setupResponse.StatusCode)
	require.Equal(t, "/apps/github?github_app=installed", setupResponse.Header.Get("Location"))

	installations, err := runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, admin.User.ID, workspaceID)
	require.NoError(t, err)
	require.Len(t, installations, 1)
	require.Equal(t, "github", installations[0].AppSlug)
	require.Len(t, installations[0].Connections, 1)
	require.Equal(t, "789", installations[0].Connections[0].ExternalSubjectRef)

	stateUpdate, cookieUpdate := startInstall()
	request, err = http.NewRequest(http.MethodGet, server.URL+"/api/github/setup?installation_id=790&setup_action=update&state="+url.QueryEscape(stateUpdate), nil)
	require.NoError(t, err)
	request.Header.Set("Cookie", cookieUpdate)
	updateResponse, err := redirectClient.Do(request)
	require.NoError(t, err)
	t.Cleanup(func() { _ = updateResponse.Body.Close() })
	require.Equal(t, http.StatusFound, updateResponse.StatusCode)
	require.Equal(t, "/apps/github?github_app=installed", updateResponse.Header.Get("Location"))

	installations, err = runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, admin.User.ID, workspaceID)
	require.NoError(t, err)
	require.Len(t, installations, 1)
	require.Len(t, installations[0].Connections, 2)
	require.Equal(t, "789", installations[0].Connections[0].ExternalSubjectRef)
	require.Equal(t, "790", installations[0].Connections[1].ExternalSubjectRef)
}

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

	privateKeyPEM := testGitHubPrivateKeyPEM(t)
	github := newGitHubFlowTestServer(t, privateKeyPEM)
	defer github.Close()

	runtime := StartRuntimeWithGitHubEndpoints(t, github.URL(), github.URL())

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()
	admin := RegisterUserForOrganizationTests(t, client, server.URL, "github-install-requested@example.com", "github-install-requested")

	var workspaces []dto.WorkspaceResponse
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(admin.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	requestAndApproveProviderAppForTest(t, runtime, admin.User.ID, workspaceID, "github")

	_, err := runtime.Store.App().Provider().SaveGitHubAppConfigForWorkspaceAdmin(runtime.Ctx, admin.User.ID, workspaceID, provider.UpsertGitHubAppConfigParams{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		AppName:       "AgentTeam HTTP",
		HTMLURL:       "https://github.com/apps/agentteam-http",
		ClientID:      "Iv1.http",
		ClientSecret:  "client-secret",
		PrivateKeyPEM: privateKeyPEM,
		WebhookSecret: "webhook-secret",
		OwnerLogin:    "acme",
		OwnerID:       7,
	})
	require.NoError(t, err)

	var started struct {
		InstallURL string `json:"installUrl"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/installation", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &started)
	require.Equal(t, http.StatusOK, response.StatusCode)

	parsedInstallURL, err := url.Parse(started.InstallURL)
	require.NoError(t, err)

	redirectClient := noRedirectHTTPClient(client)
	request, err := http.NewRequest(http.MethodGet, server.URL+"/api/github/setup?setup_action=request&state="+url.QueryEscape(parsedInstallURL.Query().Get("state")), nil)
	require.NoError(t, err)
	request.Header.Set("Cookie", response.Header.Get("Set-Cookie"))

	setupResponse, err := redirectClient.Do(request)
	require.NoError(t, err)
	t.Cleanup(func() { _ = setupResponse.Body.Close() })
	require.Equal(t, http.StatusFound, setupResponse.StatusCode)
	require.Equal(t, "/apps/github?github_app=requested", setupResponse.Header.Get("Location"))

	installations, err := runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, admin.User.ID, workspaceID)
	require.NoError(t, err)
	require.Len(t, installations, 1)
	require.Empty(t, installations[0].Connections)
}

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

	runtime := StartRuntime(t)

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()

	var apiError struct {
		Error string `json:"error"`
	}
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/github/setup", nil, nil, &apiError)
	require.Equal(t, http.StatusBadRequest, response.StatusCode)
	require.Equal(t, "Invalid GitHub setup callback", apiError.Error)
}

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

	privateKeyPEM := testGitHubPrivateKeyPEM(t)
	github := newGitHubFlowTestServer(t, privateKeyPEM)
	defer github.Close()

	runtime := StartRuntimeWithGitHubEndpoints(t, github.URL(), github.URL())

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()
	admin := RegisterUserForOrganizationTests(t, client, server.URL, "github-connections-admin@example.com", "github-connections-admin")
	member := RegisterUserForOrganizationTests(t, client, server.URL, "github-connections-member@example.com", "github-connections-member")

	var workspaces []dto.WorkspaceResponse
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(admin.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	_, err := runtime.Pool.Exec(runtime.Ctx, `
INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at)
VALUES ('mem_github_connections_member', $1, 'human', 'GitHub Connections Member', $2, 'member', 'active', NOW(), NOW())
`, workspaceID, member.User.ID)
	require.NoError(t, err)
	requestAndApproveProviderAppForTest(t, runtime, admin.User.ID, workspaceID, "github")

	_, err = runtime.Store.App().Provider().SaveGitHubAppConfigForWorkspaceAdmin(runtime.Ctx, admin.User.ID, workspaceID, provider.UpsertGitHubAppConfigParams{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		AppName:       "AgentTeam HTTP",
		HTMLURL:       "https://github.com/apps/agentteam-http",
		ClientID:      "Iv1.http",
		ClientSecret:  "client-secret",
		PrivateKeyPEM: privateKeyPEM,
		WebhookSecret: "webhook-secret",
		OwnerLogin:    "acme",
		OwnerID:       7,
	})
	require.NoError(t, err)

	var discovered []dto.GitHubInstallationResponse
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/installations", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &discovered)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, "no-store", response.Header.Get("Cache-Control"))
	require.Equal(t, []dto.GitHubInstallationResponse{
		{InstallationID: "789", AccountLogin: "acme", TargetType: "Organization"},
		{InstallationID: "790", AccountLogin: "beta", TargetType: "Organization"},
	}, discovered)

	discovered = nil
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/installations", nil, WorkspaceHeaders(member.AccessToken, workspaceID), &discovered)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, "no-store", response.Header.Get("Cache-Control"))
	require.Equal(t, []dto.GitHubInstallationResponse{
		{InstallationID: "789", AccountLogin: "acme", TargetType: "Organization"},
		{InstallationID: "790", AccountLogin: "beta", TargetType: "Organization"},
	}, discovered, "active members see only the current workspace App's minimal eligible installation inventory")

	var connections []provider.AppConnectionView
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/connections", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &connections)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Empty(t, connections, "discovery must not create AgentTeam connections")

	for _, installationID := range []string{"789", "790"} {
		var installed dto.ProviderAppInstallationResponse
		response = DoJSON(t, client, http.MethodPost, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/connections", map[string]any{
			"installation_id": installationID,
		}, WorkspaceHeaders(admin.AccessToken, workspaceID), &installed)
		require.Equal(t, http.StatusOK, response.StatusCode)
		require.True(t, installed.Enabled)
		require.True(t, slices.ContainsFunc(installed.Connections, func(connection dto.ProviderAppConnectionResponse) bool {
			return connection.Enabled && connection.ExternalSubjectRef == installationID
		}), "explicit connection response must contain the selected provider installation")
	}

	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/connections", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &connections)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, connections, 2)
	require.Equal(t, "789", connections[0].ExternalSubjectRef)
	require.Equal(t, "790", connections[1].ExternalSubjectRef)
	require.Equal(t, "acme", connections[0].DisplayLabel)
	require.Equal(t, "acme", connections[1].DisplayLabel)
	require.False(t, connections[0].RequiresUpgrade)
	require.Empty(t, connections[0].MissingPermissions)
	require.Empty(t, connections[0].MissingEvents)
	require.False(t, connections[1].RequiresUpgrade)
	require.Empty(t, connections[1].MissingPermissions)
	require.Empty(t, connections[1].MissingEvents)

	response = DoJSON(t, client, http.MethodDelete, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/connections/"+connections[0].ID, nil, WorkspaceHeaders(admin.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusNoContent, response.StatusCode)

	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/connections", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &connections)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, connections, 1)
	require.Equal(t, "790", connections[0].ExternalSubjectRef)

	response = DoJSON(t, client, http.MethodDelete, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/connections/"+connections[0].ID, nil, WorkspaceHeaders(admin.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusNoContent, response.StatusCode)

	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/connections", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &connections)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Empty(t, connections)

	installations, err := runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, admin.User.ID, workspaceID)
	require.NoError(t, err)
	require.Len(t, installations, 1)
	require.True(t, installations[0].Enabled)
	require.Empty(t, installations[0].Connections)
}

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

	privateKeyPEM := testGitHubPrivateKeyPEM(t)
	github := newGitHubFlowTestServer(t, privateKeyPEM)
	defer github.Close()

	runtime := StartRuntimeWithGitHubEndpoints(t, github.URL(), github.URL())

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()
	admin := RegisterUserForOrganizationTests(t, client, server.URL, "github-install-update-missing@example.com", "github-install-update-missing")

	var workspaces []dto.WorkspaceResponse
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(admin.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	requestAndApproveProviderAppForTest(t, runtime, admin.User.ID, workspaceID, "github")

	_, err := runtime.Store.App().Provider().SaveGitHubAppConfigForWorkspaceAdmin(runtime.Ctx, admin.User.ID, workspaceID, provider.UpsertGitHubAppConfigParams{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		AppName:       "AgentTeam HTTP",
		HTMLURL:       "https://github.com/apps/agentteam-http",
		ClientID:      "Iv1.http",
		ClientSecret:  "client-secret",
		PrivateKeyPEM: privateKeyPEM,
		WebhookSecret: "webhook-secret",
		OwnerLogin:    "acme",
		OwnerID:       7,
	})
	require.NoError(t, err)

	var started struct {
		InstallURL string `json:"installUrl"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/installation", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &started)
	require.Equal(t, http.StatusOK, response.StatusCode)

	parsedInstallURL, err := url.Parse(started.InstallURL)
	require.NoError(t, err)

	redirectClient := noRedirectHTTPClient(client)
	request, err := http.NewRequest(http.MethodGet, server.URL+"/api/github/setup?installation_id=790&setup_action=install&state="+url.QueryEscape(parsedInstallURL.Query().Get("state")), nil)
	require.NoError(t, err)

	setupResponse, err := redirectClient.Do(request)
	require.NoError(t, err)
	t.Cleanup(func() { _ = setupResponse.Body.Close() })
	require.Equal(t, http.StatusFound, setupResponse.StatusCode)
	require.Equal(t, "/apps/github?github_app=approved", setupResponse.Header.Get("Location"))

	installations, err := runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, admin.User.ID, workspaceID)
	require.NoError(t, err)
	require.Len(t, installations, 1)
	require.Empty(t, installations[0].Connections)

	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/installation", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &started)
	require.Equal(t, http.StatusOK, response.StatusCode)

	parsedInstallURL, err = url.Parse(started.InstallURL)
	require.NoError(t, err)

	request, err = http.NewRequest(http.MethodGet, server.URL+"/api/github/setup?installation_id=790&setup_action=update&state="+url.QueryEscape(parsedInstallURL.Query().Get("state")), nil)
	require.NoError(t, err)
	request.Header.Set("Cookie", response.Header.Get("Set-Cookie"))

	setupResponse, err = redirectClient.Do(request)
	require.NoError(t, err)
	t.Cleanup(func() { _ = setupResponse.Body.Close() })
	require.Equal(t, http.StatusFound, setupResponse.StatusCode)
	require.Equal(t, "/apps/github?github_app=installed", setupResponse.Header.Get("Location"))

	installations, err = runtime.Store.App().ListAppInstallationsForUser(runtime.Ctx, admin.User.ID, workspaceID)
	require.NoError(t, err)
	require.Len(t, installations, 1)
	require.Len(t, installations[0].Connections, 1)
	require.Equal(t, "790", installations[0].Connections[0].ExternalSubjectRef)
}

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

	privateKeyPEM := testGitHubPrivateKeyPEM(t)
	github := newGitHubFlowTestServer(t, privateKeyPEM)
	defer github.Close()

	runtime := StartRuntimeWithGitHubEndpoints(t, github.URL(), github.URL())

	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()

	client := server.Client()
	admin := RegisterUserForOrganizationTests(t, client, server.URL, "github-invalid-state@example.com", "github-invalid-state")

	var workspaces []dto.WorkspaceResponse
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(admin.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	requestAndApproveProviderAppForTest(t, runtime, admin.User.ID, workspaceID, "github")

	_, err := runtime.Store.App().Provider().SaveGitHubAppConfigForWorkspaceAdmin(runtime.Ctx, admin.User.ID, workspaceID, provider.UpsertGitHubAppConfigParams{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		AppName:       "AgentTeam HTTP",
		HTMLURL:       "https://github.com/apps/agentteam-http",
		ClientID:      "Iv1.http",
		ClientSecret:  "client-secret",
		PrivateKeyPEM: privateKeyPEM,
		WebhookSecret: "webhook-secret",
		OwnerLogin:    "acme",
		OwnerID:       7,
	})
	require.NoError(t, err)

	var started struct {
		InstallURL string `json:"installUrl"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces/"+workspaceID+"/providers/github/installation", nil, WorkspaceHeaders(admin.AccessToken, workspaceID), &started)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var apiError struct {
		Error string `json:"error"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+"/api/github/setup?installation_id=790&setup_action=install&state=wrong-state", nil, map[string]string{
		"Cookie": response.Header.Get("Set-Cookie"),
	}, &apiError)
	require.Equal(t, http.StatusBadRequest, response.StatusCode)
	require.Equal(t, "Invalid GitHub installation state", apiError.Error)
}

type githubFlowTestServer struct {
	server *httptest.Server

	mu                  sync.Mutex
	webhookUpdateCount  int
	lastWebhookURL      string
	lastWebhookSecret   string
	lastWebhookMimeType string
}

func newGitHubFlowTestServer(t *testing.T, privateKeyPEM string) *githubFlowTestServer {
	t.Helper()

	const ownerLogin = "acme"
	testServer := &githubFlowTestServer{}
	mux := http.NewServeMux()
	mux.HandleFunc("/apps/agentteam-http", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":        4242,
			"slug":      "agentteam-http",
			"name":      "AgentTeam HTTP",
			"client_id": "Iv1.http",
			"html_url":  "https://github.com/apps/agentteam-http",
			"owner": map[string]any{
				"login": ownerLogin,
				"id":    7,
			},
		})
	})
	mux.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Contains(t, r.Header.Get("Authorization"), "Bearer ")
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":        4242,
			"slug":      "agentteam-http",
			"name":      "AgentTeam HTTP",
			"client_id": "Iv1.http",
			"html_url":  "https://github.com/apps/agentteam-http",
			"owner": map[string]any{
				"login": ownerLogin,
				"id":    7,
			},
		})
	})
	mux.HandleFunc("/app-manifests/test-manifest-code/conversions", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPost, r.Method)
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":             4242,
			"slug":           "agentteam-http",
			"name":           "AgentTeam HTTP",
			"client_id":      "Iv1.http",
			"client_secret":  "client-secret",
			"pem":            privateKeyPEM,
			"webhook_secret": "webhook-secret",
			"html_url":       "https://github.com/apps/agentteam-http",
			"owner": map[string]any{
				"login": ownerLogin,
				"id":    7,
			},
		})
	})
	mux.HandleFunc("/app/hook/config", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodPatch, r.Method)
		require.Contains(t, r.Header.Get("Authorization"), "Bearer ")

		var payload struct {
			URL         string `json:"url"`
			ContentType string `json:"content_type"`
			Secret      string `json:"secret"`
		}
		require.NoError(t, json.NewDecoder(r.Body).Decode(&payload))

		testServer.mu.Lock()
		testServer.webhookUpdateCount++
		testServer.lastWebhookURL = payload.URL
		testServer.lastWebhookSecret = payload.Secret
		testServer.lastWebhookMimeType = payload.ContentType
		testServer.mu.Unlock()

		w.WriteHeader(http.StatusNoContent)
	})
	mux.HandleFunc("/app/installations", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Contains(t, r.Header.Get("Authorization"), "Bearer ")
		_ = json.NewEncoder(w).Encode([]map[string]any{
			{
				"id":          789,
				"app_id":      4242,
				"app_slug":    "agentteam-http",
				"html_url":    "https://github.com/settings/installations/789",
				"target_type": "Organization",
				"permissions": GitHubManifestPermissions(),
				"events":      GitHubManifestEvents(),
				"account": map[string]any{
					"login": "acme",
					"id":    7,
				},
			},
			{
				"id":          790,
				"app_id":      4242,
				"app_slug":    "agentteam-http",
				"html_url":    "https://github.com/settings/installations/790",
				"target_type": "Organization",
				"permissions": GitHubManifestPermissions(),
				"events":      GitHubManifestEvents(),
				"account": map[string]any{
					"login": "beta",
					"id":    8,
				},
			},
		})
	})
	mux.HandleFunc("/app/installations/789", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Contains(t, r.Header.Get("Authorization"), "Bearer ")
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":          789,
			"app_id":      4242,
			"app_slug":    "agentteam-http",
			"html_url":    "https://github.com/settings/installations/789",
			"target_type": "Organization",
			"permissions": GitHubManifestPermissions(),
			"events":      GitHubManifestEvents(),
			"account": map[string]any{
				"login": "acme",
				"id":    7,
			},
		})
	})
	mux.HandleFunc("/app/installations/790", func(w http.ResponseWriter, r *http.Request) {
		require.Equal(t, http.MethodGet, r.Method)
		require.Contains(t, r.Header.Get("Authorization"), "Bearer ")
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":          790,
			"app_id":      4242,
			"app_slug":    "agentteam-http",
			"html_url":    "https://github.com/settings/installations/790",
			"target_type": "Organization",
			"permissions": GitHubManifestPermissions(),
			"events":      GitHubManifestEvents(),
			"account": map[string]any{
				"login": "acme",
				"id":    7,
			},
		})
	})

	testServer.server = httptest.NewServer(mux)
	return testServer
}

func (server *githubFlowTestServer) URL() string {
	return server.server.URL
}

func (server *githubFlowTestServer) Close() {
	server.server.Close()
}

func (server *githubFlowTestServer) webhookConfigState() (count int, webhookURL string, secret string, contentType string) {
	server.mu.Lock()
	defer server.mu.Unlock()
	return server.webhookUpdateCount, server.lastWebhookURL, server.lastWebhookSecret, server.lastWebhookMimeType
}

func testGitHubPrivateKeyPEM(t *testing.T) string {
	t.Helper()

	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	require.NoError(t, err)
	return string(pem.EncodeToMemory(&pem.Block{
		Type:  "RSA PRIVATE KEY",
		Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
	}))
}

func noRedirectHTTPClient(base *http.Client) *http.Client {
	transport := http.DefaultTransport
	if base.Transport != nil {
		transport = base.Transport
	}
	return &http.Client{
		Transport: transport,
		CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
}
