package provider

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/testkit/httputil"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/provider"
	"github.com/tidbcloud/AgentTeam/internal/store/storeerr"
)

type githubProviderRoundTripFunc func(*http.Request) (*http.Response, error)

func (roundTrip githubProviderRoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
	return roundTrip(request)
}

type recordingRequestProjector struct {
	mu      sync.Mutex
	calls   int
	request app.AppInstallationRequestView
	err     error
}

func (projector *recordingRequestProjector) ProjectPendingAppInstallationRequest(
	_ context.Context,
	request app.AppInstallationRequestView,
) ([]app.AppNotificationResult, error) {
	projector.mu.Lock()
	defer projector.mu.Unlock()
	projector.calls++
	projector.request = request
	return nil, projector.err
}

func (projector *recordingRequestProjector) snapshot() (int, app.AppInstallationRequestView) {
	projector.mu.Lock()
	defer projector.mu.Unlock()
	return projector.calls, projector.request
}

// newGitHubMockServer mounts a small GitHub API mock with the endpoints used
// by the provider handlers for register/install/setup flows.
func newGitHubMockServer(t *testing.T) *httptest.Server {
	t.Helper()
	mux := http.NewServeMux()
	mux.HandleFunc("/apps/agentteam-http", func(w http.ResponseWriter, _ *http.Request) {
		_ = 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": "acme", "id": 7},
		})
	})
	mux.HandleFunc("/app/installations/789", func(w http.ResponseWriter, _ *http.Request) {
		_ = 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", func(w http.ResponseWriter, _ *http.Request) {
		_ = 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-manifests/test-manifest-code/conversions", func(w http.ResponseWriter, _ *http.Request) {
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":             4242,
			"slug":           "agentteam-http",
			"name":           "AgentTeam HTTP",
			"client_id":      "Iv1.http",
			"client_secret":  "client-secret",
			"pem":            "private-key",
			"webhook_secret": "webhook-secret",
			"html_url":       "https://github.com/apps/agentteam-http",
			"owner":          map[string]any{"login": "acme", "id": 7},
		})
	})
	return httptest.NewServer(mux)
}

type githubRecoveryMockStats struct {
	hookPatches      atomic.Int32
	installationList atomic.Int32
	installationGet  atomic.Int32
	failHookPatch    atomic.Bool
	lastHookSecret   atomic.Value
}

func newGitHubRecoveryMockServer(t *testing.T) (*httptest.Server, *githubRecoveryMockStats) {
	t.Helper()
	stats := &githubRecoveryMockStats{}
	app := map[string]any{
		"id":        4242,
		"slug":      "agentteam-http",
		"name":      "AgentTeam HTTP",
		"client_id": "Iv1.http",
		"html_url":  "https://github.example.com/apps/agentteam-http",
		"owner":     map[string]any{"login": "acme", "id": 7},
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/apps/agentteam-http", func(w http.ResponseWriter, _ *http.Request) {
		_ = json.NewEncoder(w).Encode(app)
	})
	mux.HandleFunc("/app", func(w http.ResponseWriter, _ *http.Request) {
		_ = json.NewEncoder(w).Encode(app)
	})
	mux.HandleFunc("/app/hook/config", func(w http.ResponseWriter, request *http.Request) {
		if request.Method != http.MethodPatch {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}
		stats.hookPatches.Add(1)
		var payload map[string]string
		if err := json.NewDecoder(request.Body).Decode(&payload); err == nil {
			stats.lastHookSecret.Store(payload["secret"])
		}
		if stats.failHookPatch.Load() {
			http.Error(w, "patch failed", http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusNoContent)
	})
	mux.HandleFunc("/app/installations", func(w http.ResponseWriter, _ *http.Request) {
		stats.installationList.Add(1)
		_ = json.NewEncoder(w).Encode([]any{
			map[string]any{
				"id":          789,
				"app_id":      4242,
				"app_slug":    "agentteam-http",
				"html_url":    "https://github.example.com/settings/installations/789",
				"target_type": "Organization",
				"permissions": githubManifestPermissions(),
				"events":      githubManifestEvents(),
				"account":     map[string]any{"login": "acme", "id": 7},
			},
		})
	})
	mux.HandleFunc("/app/installations/789", func(w http.ResponseWriter, _ *http.Request) {
		stats.installationGet.Add(1)
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id":          789,
			"app_id":      4242,
			"app_slug":    "agentteam-http",
			"html_url":    "https://github.example.com/settings/installations/789",
			"target_type": "Organization",
			"permissions": githubManifestPermissions(),
			"events":      githubManifestEvents(),
			"account":     map[string]any{"login": "acme", "id": 7},
		})
	})
	return httptest.NewServer(mux), stats
}

func newGitHubTestDeps(t *testing.T, fake *fakeStore, blocker ServiceEventSubscriptionBlocker, mockURL string) Dependencies {
	t.Helper()
	return Dependencies{
		Store:                           fake,
		GitHubAdminStore:                fake,
		ServiceEventSubscriptionBlocker: blocker,
		GitHubConfig: GitHubConfig{
			FlowStateKey: []byte("0123456789abcdef0123456789abcdef"),
			BaseURL:      "https://github.example.com",
			APIBaseURL:   mockURL,
		},
		HTTPClient:    http.DefaultClient,
		PublicBaseURL: "http://agentteam.example.com",
	}
}

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

	mock := newGitHubMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/registration", nil)
	require.Equal(t, http.StatusOK, resp.Code)

	var body map[string]any
	require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &body))
	require.Contains(t, body["formAction"], "/settings/apps/new?state=")
	require.NotContains(t, body["formAction"], "/organizations/")
	require.Contains(t, body, "manifest")
	require.Equal(t, "no-store", resp.Header().Get("Cache-Control"))
}

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

	fake := newFakeStore()
	fake.authorizeRegisterErr = storeerr.ErrForbidden
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/registration", nil)
	require.Equal(t, http.StatusForbidden, resp.Code)
}

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

	fake := newFakeStore()
	fake.authorizeInstallErr = storeerr.ErrConflict
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/registration", nil)
	require.Equal(t, http.StatusOK, resp.Code)
	require.NotEmpty(t, resp.Header().Get("Set-Cookie"))
	require.Equal(t, 0, fake.githubSaveCalls)
}

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

	fake := newFakeStore()
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, "http://127.0.0.1:1"))
	started := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/registration", nil)
	require.Equal(t, http.StatusOK, started.Code)
	var body struct {
		FormAction string `json:"formAction"`
	}
	require.NoError(t, json.Unmarshal(started.Body.Bytes(), &body))
	formAction, err := url.Parse(body.FormAction)
	require.NoError(t, err)
	state := formAction.Query().Get("state")
	require.NotEmpty(t, state)

	callback := httputil.DoRaw(t, router, http.MethodGet, "/api/github/callback?code=secret-code&state="+url.QueryEscape(state), http.Header{
		"Cookie": {started.Header().Get("Set-Cookie")},
	}, nil)
	require.Equal(t, http.StatusBadGateway, callback.Code)
	require.Equal(t, 0, fake.githubSaveCalls)
}

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

	mock := newGitHubMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	blocker := &httputil.FakeBlocker{}
	router := newProviderRouter(newGitHubTestDeps(t, fake, blocker, mock.URL))
	started := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/registration", nil)
	require.Equal(t, http.StatusOK, started.Code)
	var body struct {
		FormAction string `json:"formAction"`
	}
	require.NoError(t, json.Unmarshal(started.Body.Bytes(), &body))
	formAction, err := url.Parse(body.FormAction)
	require.NoError(t, err)
	state := formAction.Query().Get("state")
	require.NotEmpty(t, state)

	callback := httputil.DoRaw(t, router, http.MethodGet, "/api/github/callback?code=test-manifest-code&state="+url.QueryEscape(state), http.Header{
		"Cookie": {started.Header().Get("Set-Cookie")},
	}, nil)
	require.Equal(t, http.StatusFound, callback.Code, callback.Body.String())
	require.Equal(t, "/apps/github?github_app=registered", callback.Header().Get("Location"))
	require.Equal(t, 1, fake.githubSaveCalls)
	require.Empty(t, blocker.UnblockCalls, "registering credentials without a connection must not unblock GitHub subscriptions")
}

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

	fake := newFakeStore()
	fake.githubSetupMode = app.GitHubAppSetupModeRequester
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, "http://127.0.0.1:1"))
	started := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/registration", nil)
	require.Equal(t, http.StatusOK, started.Code)
	var body struct {
		FormAction string `json:"formAction"`
	}
	require.NoError(t, json.Unmarshal(started.Body.Bytes(), &body))
	formAction, err := url.Parse(body.FormAction)
	require.NoError(t, err)
	state := formAction.Query().Get("state")
	require.NotEmpty(t, state)

	// The signed state was issued for the requester path. Becoming an admin
	// must not silently turn the callback into a privileged direct install.
	fake.githubSetupMode = app.GitHubAppSetupModeAdmin
	callback := httputil.DoRaw(t, router, http.MethodGet, "/api/github/callback?code=secret-code&state="+url.QueryEscape(state), http.Header{
		"Cookie": {started.Header().Get("Set-Cookie")},
	}, nil)
	require.Equal(t, http.StatusConflict, callback.Code)
	require.Contains(t, callback.Body.String(), "permissions changed")
	require.Equal(t, 0, fake.githubSaveCalls)
	require.Equal(t, 0, fake.githubProposalSaveCalls)
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))

	// Missing code/state.
	resp := httputil.DoRaw(t, router, http.MethodGet, "/api/github/callback", nil, nil)
	require.Equal(t, http.StatusBadRequest, resp.Code)
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))

	resp := httputil.DoRaw(t, router, http.MethodGet, "/api/github/callback?code=abc&state=xyz", nil, nil)
	require.Equal(t, http.StatusBadRequest, resp.Code)
}

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

	fake := newFakeStore()
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/installation", nil)
	require.Equal(t, http.StatusBadRequest, resp.Code)
	require.Contains(t, resp.Body.String(), "GitHub App is not registered")
}

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

	fake := newFakeStore()
	fake.githubLoadConfig = provider.GitHubAppConfig{AppSlug: "agentteam-http", AppID: 4242}
	fake.githubLoadFound = true
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/installation", nil)
	require.Equal(t, http.StatusOK, resp.Code)
	require.Contains(t, resp.Body.String(), "installUrl")
}

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

	mock := newGitHubMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	fake.githubConnectionMode = app.GitHubAppSetupModeRequester
	fake.githubLoadConfig = provider.GitHubAppConfig{
		ID:            "ghcfg_1",
		WorkspaceID:   "wsp_1",
		AppID:         4242,
		AppSlug:       "agentteam-http",
		PrivateKeyPEM: generateTestPEM(t),
		UpdatedAt:     time.Now().UTC(),
	}
	fake.githubLoadFound = true
	fake.githubSetupResult = provider.AppInstallationView{ID: "appinst_github", AppID: "app_github", Enabled: true}
	projector := &recordingRequestProjector{}
	deps := newGitHubTestDeps(t, fake, nil, mock.URL)
	deps.InstallationRequestProjector = projector
	router := newProviderRouter(deps)

	started := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/installation", nil)
	require.Equal(t, http.StatusOK, started.Code, started.Body.String())
	require.Equal(t, "no-store", started.Header().Get("Cache-Control"))
	var startedBody struct {
		InstallURL string `json:"installUrl"`
	}
	require.NoError(t, json.Unmarshal(started.Body.Bytes(), &startedBody))
	installURL, err := url.Parse(startedBody.InstallURL)
	require.NoError(t, err)
	state := installURL.Query().Get("state")
	require.NotEmpty(t, state)

	callback := httputil.DoRaw(t, router, http.MethodGet,
		"/api/github/setup?installation_id=789&setup_action=install&state="+url.QueryEscape(state),
		http.Header{"Cookie": {started.Header().Get("Set-Cookie")}}, nil)
	require.Equal(t, http.StatusFound, callback.Code, callback.Body.String())
	require.Equal(t, "/apps/github?github_app=installed", callback.Header().Get("Location"))
	require.Equal(t, "no-store", callback.Header().Get("Cache-Control"))
	require.Equal(t, 1, fake.githubConnectionPrepareCalls)
	require.Equal(t, []string{"789"}, fake.lastGithubConnectionPrepareIDs)
	require.Equal(t, 1, fake.githubSetupCalls)
	require.Equal(t, []provider.GitHubVerifiedConnectionTarget{{
		InstallationID: "789",
		DisplayLabel:   "acme",
		AccountID:      7,
	}}, fake.lastGithubVerifiedTargets)
	require.Zero(t, fake.githubConnectionRequestCalls)
	projectionCalls, _ := projector.snapshot()
	require.Zero(t, projectionCalls)
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))

	// installation_id and state both missing.
	resp := httputil.DoRaw(t, router, http.MethodGet, "/api/github/setup", nil, nil)
	require.Equal(t, http.StatusBadRequest, resp.Code)
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))

	// installation_id missing but state present and setup_action=request.
	resp := httputil.DoRaw(t, router, http.MethodGet, "/api/github/setup?state=abc&setup_action=request", nil, nil)
	require.Equal(t, http.StatusFound, resp.Code)
	require.Equal(t, "/apps/github?github_app=requested", resp.Header().Get("Location"))
	require.Equal(t, "no-store", resp.Header().Get("Cache-Control"))
}

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

	var providerCalls atomic.Int32
	github := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		providerCalls.Add(1)
		http.Error(w, "provider lookup must not run", http.StatusInternalServerError)
	}))
	defer github.Close()

	fake := newFakeStore()
	fake.githubLoadConfig = provider.GitHubAppConfig{
		ID:            "ghcfg_1",
		WorkspaceID:   "wsp_1",
		AppID:         4242,
		AppSlug:       "agentteam-http",
		PrivateKeyPEM: generateTestPEM(t),
		UpdatedAt:     time.Now().UTC(),
	}
	fake.githubLoadFound = true
	fake.githubConnectionPrepareErr = storeerr.Conflict("GitHub connection changed. Restart installation.")
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, github.URL))

	started := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/installation", nil)
	require.Equal(t, http.StatusOK, started.Code, started.Body.String())
	var body struct {
		InstallURL string `json:"installUrl"`
	}
	require.NoError(t, json.Unmarshal(started.Body.Bytes(), &body))
	installURL, err := url.Parse(body.InstallURL)
	require.NoError(t, err)
	state := installURL.Query().Get("state")
	require.NotEmpty(t, state)

	callback := httputil.DoRaw(t, router, http.MethodGet,
		"/api/github/setup?installation_id=789&setup_action=install&state="+url.QueryEscape(state),
		http.Header{"Cookie": {started.Header().Get("Set-Cookie")}}, nil)
	require.Equal(t, http.StatusConflict, callback.Code, callback.Body.String())
	require.Equal(t, 1, fake.githubConnectionPrepareCalls)
	require.Equal(t, "789", fake.lastGithubConnectionPrepare)
	require.Zero(t, providerCalls.Load(), "the optimistic base must be captured before remote GitHub I/O")
	require.Zero(t, fake.githubSetupCalls)
}

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

	fake := newFakeStore()
	fake.githubConnectionMode = app.GitHubAppSetupModeRequester
	fake.githubLoadConfig = provider.GitHubAppConfig{AppSlug: "agentteam-http", AppID: 4242}
	fake.githubLoadFound = true
	projector := &recordingRequestProjector{}
	deps := newGitHubTestDeps(t, fake, nil, "")
	deps.InstallationRequestProjector = projector
	router := newProviderRouter(deps)

	started := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/installation", nil)
	require.Equal(t, http.StatusOK, started.Code, started.Body.String())
	var body struct {
		InstallURL string `json:"installUrl"`
	}
	require.NoError(t, json.Unmarshal(started.Body.Bytes(), &body))
	installURL, err := url.Parse(body.InstallURL)
	require.NoError(t, err)
	state := installURL.Query().Get("state")
	require.NotEmpty(t, state)

	callback := httputil.DoRaw(t, router, http.MethodGet,
		"/api/github/setup?setup_action=request&state="+url.QueryEscape(state),
		http.Header{"Cookie": {started.Header().Get("Set-Cookie")}}, nil)
	require.Equal(t, http.StatusFound, callback.Code, callback.Body.String())
	require.Equal(t, "/apps/github?github_app=requested", callback.Header().Get("Location"))
	require.Equal(t, "no-store", callback.Header().Get("Cache-Control"))
	require.Equal(t, 0, fake.githubConnectionRequestCalls)
	require.Equal(t, 0, fake.githubSetupCalls)
	projectionCalls, _ := projector.snapshot()
	require.Equal(t, 0, projectionCalls)
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))

	resp := httputil.DoRaw(t, router, http.MethodGet, "/api/github/setup?installation_id=789&state=x&setup_action=delete", nil, nil)
	require.Equal(t, http.StatusBadRequest, resp.Code)
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))

	resp := httputil.DoRaw(t, router, http.MethodGet, "/api/github/setup?installation_id=789&state=x&setup_action=install", nil, nil)
	require.Equal(t, http.StatusFound, resp.Code)
	require.Contains(t, resp.Header().Get("Location"), "approved")
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))

	resp := httputil.DoRaw(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/connections",
		http.Header{
			"Authorization":  {"Bearer token"},
			"X-Workspace-Id": {"wsp_1"},
			"Content-Type":   {"application/json"},
		}, []byte("not json"))
	require.Equal(t, http.StatusBadRequest, resp.Code)
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/connections", map[string]any{})
	require.Equal(t, http.StatusBadRequest, resp.Code)
	require.Contains(t, resp.Body.String(), "Exactly one GitHub installation id is required")
}

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

	fake := newFakeStore()
	fake.githubLoadFound = true
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))
	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/connections", map[string]any{
		"installation_ids": []string{"1", "2"},
	})
	require.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
	require.Contains(t, resp.Body.String(), "Exactly one GitHub installation id is required")
	require.Zero(t, fake.githubConnectionPrepareCalls)
	require.Zero(t, fake.githubSetupCalls)
}

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

	fake := newFakeStore()
	fake.githubConnectionPrepareErr = storeerr.BadRequest("GitHub installation ids must be resolved positive decimal strings.")
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))
	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/connections", map[string]any{
		"installation_id": " 1 ",
	})
	require.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
	require.Equal(t, []string{" 1 "}, fake.lastGithubConnectionPrepareIDs)
	require.Zero(t, fake.githubSetupCalls)
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/connections", map[string]any{
		"installation_id": "789",
	})
	require.Equal(t, http.StatusBadRequest, resp.Code)
	require.Contains(t, resp.Body.String(), "GitHub App is not registered")
}

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

	github := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		require.Equal(t, "/app/installations/789", request.URL.Path)
		_ = json.NewEncoder(w).Encode(map[string]any{
			"id": 789, "app_id": 4242, "app_slug": "agentteam-http", "target_type": "Organization",
			"permissions": map[string]string{"pull_requests": "read", "issues": "read", "metadata": "read"},
			"events":      []string{"pull_request"}, "account": map[string]any{"login": "acme", "id": 7},
		})
	}))
	defer github.Close()
	fake := newFakeStore()
	fake.githubLoadFound = true
	fake.githubLoadConfig = provider.GitHubAppConfig{AppID: 4242, AppSlug: "agentteam-http", PrivateKeyPEM: generateTestPEM(t)}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, github.URL))

	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/connections", map[string]any{
		"installation_id": "789",
	})
	require.Equal(t, http.StatusConflict, resp.Code, resp.Body.String())
	require.Equal(t, 1, fake.githubConnectionPrepareCalls)
	require.Zero(t, fake.githubSetupCalls)
	require.Zero(t, fake.githubConnectionRequestCalls)
}

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

	mock := newGitHubMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	fake.githubConnectionMode = app.GitHubAppSetupModeRequester
	fake.githubLoadFound = true
	fake.githubLoadConfig = provider.GitHubAppConfig{
		ID:            "ghcfg_member",
		WorkspaceID:   "wsp_1",
		AppID:         4242,
		AppSlug:       "agentteam-http",
		PrivateKeyPEM: generateTestPEM(t),
		UpdatedAt:     time.Now().UTC(),
	}
	fake.githubSetupResult = provider.AppInstallationView{ID: "svcinst_member", AppID: "app_github", Enabled: true}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/connections", map[string]any{
		"installation_id": "789",
	})
	require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
	require.Contains(t, resp.Body.String(), `"id":"svcinst_member"`)
	require.Zero(t, fake.githubConnectionRequestCalls, "active member connect must not create an approval request")
	require.Equal(t, 1, fake.githubConnectionPrepareCalls)
	require.Equal(t, []string{"789"}, fake.lastGithubConnectionPrepareIDs)
	require.Equal(t, 1, fake.githubSetupCalls, "active member connect is immediately effective")
	require.Equal(t, []provider.GitHubVerifiedConnectionTarget{{
		InstallationID: "789", DisplayLabel: "acme", AccountID: 7,
	}}, fake.lastGithubVerifiedTargets)
	require.NotContains(t, resp.Body.String(), "account_id")
	require.NotContains(t, resp.Body.String(), "private_key")
	require.NotContains(t, resp.Body.String(), "webhook_secret")
}

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

	fake := newFakeStore()
	fake.githubConfigView = provider.GitHubAppConfigView{Registered: true, AppSlug: "agentteam-http"}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/config", nil)
	require.Equal(t, http.StatusOK, resp.Code)
	require.Contains(t, resp.Body.String(), "agentteam-http")
}

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

	fake := newFakeStore()
	fake.githubSetupMode = app.GitHubAppSetupModeRequester
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/setup-proposal", nil)
	require.Equal(t, http.StatusOK, resp.Code)
	require.JSONEq(t, `{"proposal":null}`, resp.Body.String())
	require.Equal(t, "no-store", resp.Header().Get("Cache-Control"))
}

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

	fake := newFakeStore()
	fake.githubSetupMode = app.GitHubAppSetupModeRequester
	fake.githubProposalFound = true
	fake.githubProposal = app.GitHubSetupProposalView{
		ID:         "ghsetup_safe",
		Ready:      true,
		AppID:      "9007199254740993",
		AppSlug:    "agentteam-http",
		AppName:    "AgentTeam HTTP",
		OwnerLogin: "acme",
		ExpiresAt:  time.Date(2026, 7, 20, 23, 0, 0, 0, time.UTC),
	}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/setup-proposal", nil)
	require.Equal(t, http.StatusOK, resp.Code)
	require.Contains(t, resp.Body.String(), `"id":"ghsetup_safe"`)
	require.Contains(t, resp.Body.String(), `"appId":"9007199254740993"`)
	require.NotContains(t, resp.Body.String(), "private_key")
	require.NotContains(t, resp.Body.String(), "webhook_secret")
	require.NotContains(t, resp.Body.String(), "ciphertext")
}

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

	fake := newFakeStore()
	fake.githubSetupMode = app.GitHubAppSetupModeAdmin
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/setup-proposal", map[string]any{
		"settings_url":    "https://github.example.com/organizations/acme/settings/apps/agentteam-http",
		"private_key_pem": generateTestPEM(t),
	})
	require.Equal(t, http.StatusForbidden, resp.Code)
	require.Equal(t, 0, fake.githubProposalSaveCalls)
	require.Equal(t, 0, fake.githubSaveCalls)
}

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

	mock, stats := newGitHubRecoveryMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	fake.githubProposalSave = app.GitHubSetupProposalView{
		ID:         "ghsetup_admin",
		Ready:      true,
		AppID:      "4242",
		AppSlug:    "agentteam-http",
		AppName:    "AgentTeam HTTP",
		OwnerLogin: "acme",
		ExpiresAt:  time.Now().UTC().Add(time.Hour),
	}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))
	privateKey := generateTestPEM(t)

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/setup-draft", map[string]any{
		"settings_url":    "https://github.example.com/organizations/acme/settings/apps/agentteam-http",
		"private_key_pem": privateKey,
	})
	require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
	require.Equal(t, "no-store", resp.Header().Get("Cache-Control"))
	require.Contains(t, resp.Body.String(), `"id":"ghsetup_admin"`)
	require.Contains(t, resp.Body.String(), `"installationId":"789"`)
	require.Contains(t, resp.Body.String(), `"accountLogin":"acme"`)
	require.NotContains(t, resp.Body.String(), privateKey)
	require.NotContains(t, resp.Body.String(), "private_key")
	require.NotContains(t, resp.Body.String(), "webhook_secret")
	require.NotContains(t, resp.Body.String(), "ciphertext")
	require.Equal(t, int32(1), stats.installationList.Load())
	require.Zero(t, stats.hookPatches.Load(), "draft discovery must not mutate the provider webhook")
	require.Equal(t, 1, fake.githubProposalSaveCalls)
	require.Zero(t, fake.githubSaveCalls)
	require.Zero(t, fake.githubSetupCalls)
	require.Equal(t, strings.TrimSpace(privateKey), fake.lastGithubProposalSave.PrivateKeyPEM)
	require.NotEmpty(t, fake.lastGithubProposalSave.WebhookSecret)
}

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

	mock, stats := newGitHubRecoveryMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	fake.githubAdminDraftConfig = provider.GitHubAppConfig{
		WorkspaceID:   "wsp_1",
		AppID:         4242,
		AppSlug:       "agentteam-http",
		PrivateKeyPEM: generateTestPEM(t),
		WebhookSecret: "draft-webhook-secret",
	}
	now := time.Now().UTC()
	fake.githubSetupResult = provider.AppInstallationView{
		ID:      "appinst_github",
		AppID:   "app_github",
		AppSlug: "github",
		AppName: "GitHub",
		Enabled: true,
		Connections: []provider.AppConnectionView{{
			ID:                 "appconn_1",
			ExternalSubjectRef: "789",
			DisplayLabel:       "acme",
			Enabled:            true,
			CreatedAt:          now,
			UpdatedAt:          now,
		}},
		CreatedAt: now,
		UpdatedAt: now,
	}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/setup-draft/complete", map[string]any{
		"setup_proposal_id": "ghsetup_admin",
		"installation_id":   "789",
	})
	require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
	require.Equal(t, "no-store", resp.Header().Get("Cache-Control"))
	require.Contains(t, resp.Body.String(), `"externalSubjectRef":"789"`)
	require.Equal(t, int32(1), stats.installationGet.Load())
	require.Equal(t, int32(1), stats.hookPatches.Load())
	require.Equal(t, "draft-webhook-secret", stats.lastHookSecret.Load())
	require.Equal(t, 1, fake.githubCompletionBeginCalls)
	require.Equal(t, 1, fake.githubCompletionMarkCalls)
	require.Equal(t, 1, fake.githubCompletionFinalizeCalls)
}

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

	var installationGets atomic.Int32
	var hookPatches atomic.Int32
	github := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		switch request.URL.Path {
		case "/app/installations/2", "/app/installations/10":
			installationGets.Add(1)
			id := int64(2)
			login := "acme"
			accountID := int64(20)
			if request.URL.Path == "/app/installations/10" {
				id, login, accountID = 10, "beta", 30
			}
			_ = json.NewEncoder(w).Encode(map[string]any{
				"id": id, "app_id": 4242, "app_slug": "agentteam-http", "target_type": "Organization",
				"permissions": githubManifestPermissions(), "events": githubManifestEvents(),
				"account": map[string]any{"login": login, "id": accountID},
			})
		case "/app/hook/config":
			require.Equal(t, int32(2), installationGets.Load(), "PATCH must wait for every exact GET")
			hookPatches.Add(1)
			w.WriteHeader(http.StatusNoContent)
		default:
			t.Fatalf("unexpected provider request %s %s", request.Method, request.URL.Path)
		}
	}))
	defer github.Close()

	fake := newFakeStore()
	fake.githubAdminDraftConfig = provider.GitHubAppConfig{
		AppID: 4242, AppSlug: "agentteam-http", PrivateKeyPEM: generateTestPEM(t), WebhookSecret: "batch-secret",
	}
	fake.githubSetupResult = provider.AppInstallationView{ID: "appinst_github", AppID: "app_github", Enabled: true}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, github.URL))
	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/setup-draft/complete", map[string]any{
		"setup_proposal_id": "ghsetup_admin",
		"installation_ids":  []string{"2", "10"},
	})
	require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
	require.Equal(t, int32(2), installationGets.Load())
	require.Equal(t, int32(1), hookPatches.Load())
	require.Equal(t, []string{"2", "10"}, fake.lastGithubCompletionIDs)
	require.Equal(t, []app.GitHubSetupCompletionTarget{
		{InstallationID: "2", AccountLogin: "acme", AccountID: 20},
		{InstallationID: "10", AccountLogin: "beta", AccountID: 30},
	}, fake.lastGithubCompletionTargets)
	require.Equal(t, 1, fake.githubCompletionMarkCalls)
	require.Equal(t, 1, fake.githubCompletionFinalizeCalls)
}

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

	var hookPatches atomic.Int32
	github := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		switch request.URL.Path {
		case "/app/installations/2":
			_ = json.NewEncoder(w).Encode(map[string]any{
				"id": 2, "app_id": 4242, "app_slug": "agentteam-http", "target_type": "Organization",
				"permissions": githubManifestPermissions(), "events": githubManifestEvents(),
				"account": map[string]any{"login": "acme", "id": 20},
			})
		case "/app/installations/10":
			http.Error(w, "temporary upstream failure", http.StatusBadGateway)
		case "/app/hook/config":
			hookPatches.Add(1)
			w.WriteHeader(http.StatusNoContent)
		default:
			t.Fatalf("unexpected provider request %s %s", request.Method, request.URL.Path)
		}
	}))
	defer github.Close()

	fake := newFakeStore()
	fake.githubAdminDraftConfig = provider.GitHubAppConfig{
		AppID: 4242, AppSlug: "agentteam-http", PrivateKeyPEM: generateTestPEM(t), WebhookSecret: "batch-secret",
	}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, github.URL))
	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/setup-draft/complete", map[string]any{
		"setup_proposal_id": "ghsetup_admin",
		"installation_ids":  []string{"2", "10"},
	})
	require.Equal(t, http.StatusBadGateway, resp.Code, resp.Body.String())
	require.Zero(t, fake.githubCompletionMarkCalls)
	require.Zero(t, fake.githubCompletionFinalizeCalls)
	require.Zero(t, hookPatches.Load())
}

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

	fake := newFakeStore()
	fake.githubCompletionErr = storeerr.BadRequest("GitHub installation ids must be resolved positive decimal strings.")
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))
	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/setup-draft/complete", map[string]any{
		"setup_proposal_id": "ghsetup_admin",
		"installation_id":   " 1 ",
	})
	require.Equal(t, http.StatusBadRequest, resp.Code, resp.Body.String())
	require.Equal(t, []string{" 1 "}, fake.lastGithubCompletionIDs)
	require.Zero(t, fake.githubCompletionMarkCalls)
	require.Zero(t, fake.githubCompletionFinalizeCalls)
}

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

	var installationGets atomic.Int32
	var hookPatches atomic.Int32
	client := &http.Client{Transport: githubProviderRoundTripFunc(func(request *http.Request) (*http.Response, error) {
		deadline, bounded := request.Context().Deadline()
		require.True(t, bounded, "the complete admin setup provider phase must have one overall deadline")
		require.LessOrEqual(t, time.Until(deadline), githubSetupCompletionTimeout+time.Second)
		if request.Method == http.MethodPatch {
			hookPatches.Add(1)
			return &http.Response{StatusCode: http.StatusNoContent, Body: http.NoBody, Header: make(http.Header)}, nil
		}
		call := installationGets.Add(1)
		if call == 2 {
			return nil, context.DeadlineExceeded
		}
		body := `{"id":2,"app_id":4242,"app_slug":"agentteam-http","target_type":"Organization","permissions":{"pull_requests":"read","issues":"read","metadata":"read"},"events":["pull_request","pull_request_review","pull_request_review_comment","issue_comment"],"account":{"login":"acme","id":20}}`
		return &http.Response{
			StatusCode: http.StatusOK,
			Header:     make(http.Header),
			Body:       io.NopCloser(strings.NewReader(body)),
			Request:    request,
		}, nil
	})}

	fake := newFakeStore()
	fake.githubAdminDraftConfig = provider.GitHubAppConfig{
		AppID: 4242, AppSlug: "agentteam-http", PrivateKeyPEM: generateTestPEM(t), WebhookSecret: "batch-secret",
	}
	deps := newGitHubTestDeps(t, fake, nil, "https://github.example.test")
	deps.HTTPClient = client
	router := newProviderRouter(deps)
	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/setup-draft/complete", map[string]any{
		"setup_proposal_id": "ghsetup_admin",
		"installation_ids":  []string{"2", "10"},
	})
	require.Equal(t, http.StatusBadGateway, resp.Code, resp.Body.String())
	require.Equal(t, int32(2), installationGets.Load())
	require.Zero(t, hookPatches.Load())
	require.Equal(t, 1, fake.githubCompletionBeginCalls)
	require.Zero(t, fake.githubCompletionMarkCalls)
	require.Zero(t, fake.githubCompletionFinalizeCalls)
}

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

	var hookPatches atomic.Int32
	github := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		switch request.URL.Path {
		case "/app/installations/789":
			_ = json.NewEncoder(w).Encode(map[string]any{
				"id":          789,
				"app_id":      4242,
				"app_slug":    "agentteam-http",
				"target_type": "Organization",
				"permissions": map[string]string{"pull_requests": "read", "metadata": "read"},
				"events":      githubManifestEvents(),
				"account":     map[string]any{"login": "acme", "id": 7},
			})
		case "/app/hook/config":
			hookPatches.Add(1)
			w.WriteHeader(http.StatusNoContent)
		default:
			t.Fatalf("unexpected provider request %s %s", request.Method, request.URL.Path)
		}
	}))
	defer github.Close()

	fake := newFakeStore()
	fake.githubAdminDraftConfig = provider.GitHubAppConfig{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		PrivateKeyPEM: generateTestPEM(t),
		WebhookSecret: "draft-webhook-secret",
	}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, github.URL))
	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/setup-draft/complete", map[string]any{
		"setup_proposal_id": "ghsetup_admin",
		"installation_id":   "789",
	})
	require.Equal(t, http.StatusConflict, resp.Code, resp.Body.String())
	require.Equal(t, 1, fake.githubCompletionBeginCalls)
	require.Zero(t, fake.githubCompletionMarkCalls)
	require.Zero(t, fake.githubCompletionFinalizeCalls)
	require.Zero(t, hookPatches.Load())
}

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

	github := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		if request.URL.Path == "/app/installations/789" {
			http.Error(w, "gone", http.StatusNotFound)
			return
		}
		t.Fatalf("unexpected provider request %s %s", request.Method, request.URL.Path)
	}))
	defer github.Close()
	fake := newFakeStore()
	fake.githubAdminDraftConfig = provider.GitHubAppConfig{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		PrivateKeyPEM: generateTestPEM(t),
		WebhookSecret: "draft-webhook-secret",
	}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, github.URL))

	resp := httputil.DoJSON(t, router, http.MethodPost, "/api/v1/workspaces/wsp_1/providers/github/setup-draft/complete", map[string]any{
		"setup_proposal_id": "ghsetup_admin",
		"installation_id":   "789",
	})
	require.Equal(t, http.StatusBadGateway, resp.Code, resp.Body.String())
	require.Equal(t, 1, fake.githubCompletionBeginCalls)
	require.Zero(t, fake.githubCompletionMarkCalls)
	require.Zero(t, fake.githubCompletionFinalizeCalls)
}

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

	mock, stats := newGitHubRecoveryMockServer(t)
	defer mock.Close()

	fake := newFakeStore()
	fake.githubSetupMode = app.GitHubAppSetupModeRequester
	fake.githubProposalSave = app.GitHubSetupProposalView{
		ID:         "ghsetup_verified",
		Ready:      true,
		AppID:      "4242",
		AppSlug:    "agentteam-http",
		AppName:    "AgentTeam HTTP",
		OwnerLogin: "acme",
		ExpiresAt:  time.Now().UTC().Add(time.Hour),
	}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))
	privateKey := generateTestPEM(t)

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/setup-proposal", map[string]any{
		"settings_url":    "https://github.example.com/organizations/acme/settings/apps/agentteam-http",
		"private_key_pem": privateKey,
	})
	require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
	require.Equal(t, "no-store", resp.Header().Get("Cache-Control"))
	require.Contains(t, resp.Body.String(), `"id":"ghsetup_verified"`)
	require.NotContains(t, resp.Body.String(), privateKey)
	require.NotContains(t, resp.Body.String(), "private_key")
	require.NotContains(t, resp.Body.String(), "webhook_secret")
	require.Zero(t, stats.hookPatches.Load(), "requester verification must not mutate the GitHub App webhook")
	require.Equal(t, int32(1), stats.installationList.Load())
	require.Contains(t, resp.Body.String(), `"installationId":"789"`)
	require.Equal(t, 1, fake.githubProposalSaveCalls)
	require.Equal(t, 0, fake.githubSaveCalls)
	require.Equal(t, 0, fake.githubSetupCalls)
	require.Equal(t, int64(4242), fake.lastGithubProposalSave.AppID)
	require.Equal(t, strings.TrimSpace(privateKey), fake.lastGithubProposalSave.PrivateKeyPEM)
	require.NotEmpty(t, fake.lastGithubProposalSave.WebhookSecret)
}

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

	mock, stats := newGitHubRecoveryMockServer(t)
	defer mock.Close()
	stats.failHookPatch.Store(true)
	fake := newFakeStore()
	fake.githubSetupMode = app.GitHubAppSetupModeRequester
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/setup-proposal", map[string]any{
		"settings_url":    "https://github.example.com/organizations/acme/settings/apps/agentteam-http",
		"private_key_pem": generateTestPEM(t),
	})
	require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
	require.Zero(t, stats.hookPatches.Load())
	require.Zero(t, fake.githubVerificationBeginCalls)
	require.Zero(t, fake.githubVerificationFinalizeCalls)
	require.Zero(t, fake.githubVerificationMarkCalls)
	require.Zero(t, fake.githubVerificationAbortCalls)
	require.Equal(t, 1, fake.githubProposalSaveCalls)
}

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

	mock, stats := newGitHubRecoveryMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	fake.githubSetupMode = app.GitHubAppSetupModeRequester
	fake.githubProposalSaveErr = errors.New("forced draft save failure")
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/setup-proposal", map[string]any{
		"settings_url":    "https://github.example.com/organizations/acme/settings/apps/agentteam-http",
		"private_key_pem": generateTestPEM(t),
	})
	require.Equal(t, http.StatusInternalServerError, resp.Code)
	require.Zero(t, fake.githubVerificationBeginCalls)
	require.Zero(t, stats.hookPatches.Load(), "requester draft preparation never PATCHes the provider")
	require.Zero(t, stats.installationList.Load(), "discovery starts only after the owner-bound draft commits")
	require.Zero(t, fake.githubVerificationFinalizeCalls)
	require.Zero(t, fake.githubVerificationAbortCalls)
}

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

	mock, stats := newGitHubRecoveryMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	fake.githubSetupMode = app.GitHubAppSetupModeRequester
	fake.githubProposalErr = errors.New("forced owner-bound reload failure")
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/setup-proposal", map[string]any{
		"settings_url":    "https://github.example.com/organizations/acme/settings/apps/agentteam-http",
		"private_key_pem": generateTestPEM(t),
	})
	require.Equal(t, http.StatusInternalServerError, resp.Code)
	require.Zero(t, stats.hookPatches.Load())
	require.Zero(t, fake.githubVerificationBeginCalls)
	require.Zero(t, fake.githubVerificationFinalizeCalls)
	require.Zero(t, fake.githubVerificationMarkCalls)
	require.Zero(t, fake.githubVerificationAbortCalls)
	require.Equal(t, 1, fake.githubProposalSaveCalls)
}

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

	mock, stats := newGitHubRecoveryMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	fake.githubSetupMode = app.GitHubAppSetupModeAdmin
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/setup-proposal", map[string]any{
		"settings_url": "https://github.example.com/organizations/acme/settings/apps/ignored", "private_key_pem": generateTestPEM(t),
	})
	require.Equal(t, http.StatusForbidden, resp.Code, resp.Body.String())
	require.Zero(t, fake.githubVerificationResumeCalls)
	require.Zero(t, fake.githubVerificationBeginCalls)
	require.Zero(t, fake.githubVerificationMarkCalls)
	require.Zero(t, fake.githubVerificationFinalizeCalls)
	require.Zero(t, stats.hookPatches.Load())
	require.Zero(t, stats.installationList.Load())
}

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

	mock := newGitHubMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	fake.githubSetupMode = app.GitHubAppSetupModeRequester
	fake.githubProposal = app.GitHubSetupProposalView{ID: "ghsetup_member", Ready: true}
	fake.githubAdminDraftConfig = provider.GitHubAppConfig{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		PrivateKeyPEM: generateTestPEM(t),
		WebhookSecret: "must-not-leak",
	}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	started := httputil.DoJSON(
		t, router, http.MethodGet,
		"/api/v1/workspaces/wsp_1/providers/github/setup-proposal/ghsetup_member/installation", nil,
	)
	require.Equal(t, http.StatusOK, started.Code, started.Body.String())
	require.Equal(t, "no-store", started.Header().Get("Cache-Control"))
	var body struct {
		InstallURL string `json:"installUrl"`
	}
	require.NoError(t, json.Unmarshal(started.Body.Bytes(), &body))
	installURL, err := url.Parse(body.InstallURL)
	require.NoError(t, err)
	require.Equal(t, "/apps/agentteam-http/installations/new", installURL.Path)
	nonce := installURL.Query().Get("state")
	require.NotEmpty(t, nonce)
	cookies := started.Result().Cookies()
	require.NotEmpty(t, cookies)

	callback := httptest.NewRecorder()
	request := httptest.NewRequest(
		http.MethodGet,
		"/api/github/setup?installation_id=789&setup_action=install&state="+url.QueryEscape(nonce),
		nil,
	)
	for _, cookie := range cookies {
		request.AddCookie(cookie)
	}
	router.ServeHTTP(callback, request)
	require.Equal(t, http.StatusFound, callback.Code, callback.Body.String())
	require.Equal(t, "/apps/github?github_app=ready", callback.Header().Get("Location"))
	require.Equal(t, "no-store", callback.Header().Get("Cache-Control"))
	require.Zero(t, fake.githubConnectionRequestCalls, "proposal callback must not create a second approval request")
	require.Zero(t, fake.githubSetupCalls, "proposal callback must not activate a AgentTeam connection")
	require.Zero(t, fake.githubSaveCalls, "proposal callback must not activate provider config")
}

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

	mock, stats := newGitHubRecoveryMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	blocker := &httputil.FakeBlocker{}
	updatedAt := time.Now().UTC().Truncate(time.Second)
	fake.githubLoadFound = true
	fake.githubLoadConfig = provider.GitHubAppConfig{
		ID:            "ghcfg_existing",
		WorkspaceID:   "wsp_1",
		AppID:         4242,
		AppSlug:       "old-slug-before-rename",
		WebhookSecret: "stable-active-webhook-secret",
		UpdatedAt:     updatedAt,
	}
	fake.githubSaveResult = provider.GitHubAppConfigView{Registered: true, AppID: 4242, AppSlug: "agentteam-http"}
	router := newProviderRouter(newGitHubTestDeps(t, fake, blocker, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/config", map[string]any{
		"settings_url":    "https://github.example.com/organizations/acme/settings/apps/agentteam-http",
		"private_key_pem": generateTestPEM(t),
	})
	require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
	require.Equal(t, int32(1), stats.hookPatches.Load())
	require.Equal(t, "stable-active-webhook-secret", stats.lastHookSecret.Load())
	require.Equal(t, "stable-active-webhook-secret", fake.lastGithubVerificationInput.Config.WebhookSecret)
	require.Equal(t, "ghcfg_existing", fake.lastGithubVerificationInput.ExpectedConfigID)
	require.Equal(t, updatedAt, fake.lastGithubVerificationInput.ExpectedConfigUpdatedAt)
	require.Zero(t, fake.githubVerificationAbortCalls)
	require.Zero(t, stats.installationList.Load(), "credential recovery must not enumerate GitHub App installations")
	require.Zero(t, fake.githubSetupCalls, "credential recovery must not apply or replay GitHub connections")
	require.Empty(t, blocker.UnblockCalls, "credential recovery must not unblock all GitHub subscriptions")
}

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

	mock, stats := newGitHubRecoveryMockServer(t)
	defer mock.Close()
	fake := newFakeStore()
	fake.githubSetupMode = app.GitHubAppSetupModeRequester
	fake.authorizeInstallErr = storeerr.ErrForbidden
	fake.githubVerificationResumeFound = true
	fake.githubAdminDraftConfig = provider.GitHubAppConfig{AppID: 4242, AppSlug: "agentteam-http", PrivateKeyPEM: generateTestPEM(t)}
	fake.githubVerificationLease.WebhookSecret = "durable-admin-secret"
	fake.githubSaveResult = provider.GitHubAppConfigView{Registered: true, AppID: 4242, AppSlug: "agentteam-http"}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/config", map[string]any{
		"settings_url": "https://github.example.com/organizations/acme/settings/apps/ignored", "private_key_pem": generateTestPEM(t),
	})
	require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
	require.Equal(t, 1, fake.githubVerificationResumeCalls)
	require.Zero(t, fake.githubVerificationBeginCalls)
	require.Equal(t, 1, fake.githubVerificationMarkCalls)
	require.Equal(t, 1, fake.githubVerificationFinalizeCalls)
	require.Equal(t, int32(1), stats.hookPatches.Load())
}

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

	var providerCalls atomic.Int32
	github := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
		providerCalls.Add(1)
	}))
	defer github.Close()
	fake := newFakeStore()
	fake.authorizeInstallErr = storeerr.Conflict("Fresh GitHub setup must select an organization installation before activation")
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, github.URL))

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/config", map[string]any{
		"settings_url":    "https://github.example.com/organizations/acme/settings/apps/agentteam-http",
		"private_key_pem": generateTestPEM(t),
	})
	require.Equal(t, http.StatusConflict, resp.Code, resp.Body.String())
	require.Contains(t, resp.Body.String(), "select an organization installation")
	require.Zero(t, providerCalls.Load(), "fresh config activation must fail before provider verification")
	require.Zero(t, fake.githubVerificationBeginCalls)
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))

	resp := httputil.DoRaw(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/config",
		http.Header{
			"Authorization":  {"Bearer token"},
			"X-Workspace-Id": {"wsp_1"},
			"Content-Type":   {"application/json"},
		}, []byte("not json"))
	require.Equal(t, http.StatusBadRequest, resp.Code)
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/config", map[string]any{
		"settings_url":    "",
		"private_key_pem": "",
	})
	require.Equal(t, http.StatusBadRequest, resp.Code)
}

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

	fake := newFakeStore()
	fake.githubLoadFound = true
	fake.githubLoadConfig = provider.GitHubAppConfig{ID: "ghcfg_existing", AppID: 4242}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/config", map[string]any{
		"settings_url":    "https://github.example.com/organizations/acme/settings/apps/agentteam-http",
		"private_key_pem": "private-key",
	})
	require.Equal(t, http.StatusBadRequest, resp.Code)
	require.Contains(t, resp.Body.String(), "Invalid GitHub App private key")
	require.Equal(t, 0, fake.githubSaveCalls)
}

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

	mock := newGitHubMockServer(t)
	defer mock.Close()

	pemStr := generateTestPEM(t)
	fake := newFakeStore()
	fake.githubLoadFound = true
	fake.githubLoadConfig = provider.GitHubAppConfig{ID: "ghcfg_existing", AppID: 4242}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodPut, "/api/v1/workspaces/wsp_1/providers/github/config", map[string]any{
		"settings_url":    "https://other.example.com/organizations/acme/settings/apps/agentteam-http",
		"private_key_pem": pemStr,
	})
	require.Equal(t, http.StatusBadRequest, resp.Code)
}

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

	fake := newFakeStore()
	blocker := &httputil.FakeBlocker{}
	router := newProviderRouter(newGitHubTestDeps(t, fake, blocker, ""))

	resp := httputil.DoJSON(t, router, http.MethodDelete, "/api/v1/workspaces/wsp_1/providers/github/config", nil)
	require.Equal(t, http.StatusOK, resp.Code)
	require.Empty(t, blocker.BlockCalls, "GitHub config deletion owns subscription blocking inside its mutation transaction")
}

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

	fake := newFakeStore()
	fake.githubDeleteErr = storeerr.ErrNotFound
	blocker := &httputil.FakeBlocker{}
	router := newProviderRouter(newGitHubTestDeps(t, fake, blocker, ""))

	resp := httputil.DoJSON(t, router, http.MethodDelete, "/api/v1/workspaces/wsp_1/providers/github/config", nil)
	require.Equal(t, http.StatusNotFound, resp.Code)
	require.Empty(t, blocker.BlockCalls)
}

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

	fake := newFakeStore()
	fake.githubConnections = []provider.AppConnectionView{{ID: "cxn_1", ExternalSubjectRef: "789"}}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/connections", nil)
	require.Equal(t, http.StatusOK, resp.Code)
}

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

	mock := newGitHubMockServer(t)
	defer mock.Close()

	fake := newFakeStore()
	fake.githubLoadConfig = provider.GitHubAppConfig{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		PrivateKeyPEM: generateTestPEM(t),
		WebhookSecret: "must-not-leak",
	}
	fake.githubLoadFound = true
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/installations", nil)
	require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
	require.Equal(t, "no-store", resp.Header().Get("Cache-Control"))
	require.JSONEq(t, `[
		{"installationId":"789","accountLogin":"acme","targetType":"Organization"},
		{"installationId":"790","accountLogin":"beta","targetType":"Organization"}
	]`, resp.Body.String())
	require.NotContains(t, resp.Body.String(), "html_url")
	require.NotContains(t, resp.Body.String(), "permissions")
	require.NotContains(t, resp.Body.String(), "events")
	require.NotContains(t, resp.Body.String(), "must-not-leak")
	require.Equal(t, 0, fake.githubSetupCalls, "discovery must not create AgentTeam connections")
}

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

	mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		require.Equal(t, "/app/installations", request.URL.Path)
		_ = json.NewEncoder(w).Encode([]map[string]any{
			{
				"id": 1, "app_id": 4242, "app_slug": "agentteam-http", "target_type": "Organization",
				"permissions": map[string]string{"pull_requests": "read", "metadata": "read"},
				"events":      githubManifestEvents(), "account": map[string]any{"login": "missing-permission", "id": 1},
			},
			{
				"id": 2, "app_id": 4242, "app_slug": "agentteam-http", "target_type": "Organization",
				"permissions": githubManifestPermissions(), "events": []string{"pull_request"},
				"account": map[string]any{"login": "missing-events", "id": 2},
			},
			{
				"id": 3, "app_id": 4242, "app_slug": "agentteam-http", "target_type": "organization",
				"permissions": githubManifestPermissions(), "events": githubManifestEvents(),
				"account": map[string]any{"login": "eligible", "id": 3},
			},
		})
	}))
	defer mock.Close()

	fake := newFakeStore()
	fake.githubLoadConfig = provider.GitHubAppConfig{AppID: 4242, AppSlug: "agentteam-http", PrivateKeyPEM: generateTestPEM(t)}
	fake.githubLoadFound = true
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))
	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/installations", nil)
	require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
	require.JSONEq(t, `[{"installationId":"3","accountLogin":"eligible","targetType":"Organization"}]`, resp.Body.String())
}

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

	var providerCalls atomic.Int32
	mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		providerCalls.Add(1)
		_ = json.NewEncoder(w).Encode([]any{})
	}))
	defer mock.Close()

	fake := newFakeStore()
	fake.githubConnectionModeErr = storeerr.ErrForbidden
	fake.githubLoadConfig = provider.GitHubAppConfig{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		PrivateKeyPEM: generateTestPEM(t),
	}
	fake.githubLoadFound = true
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/installations", nil)
	require.Equal(t, http.StatusForbidden, resp.Code)
	require.Zero(t, providerCalls.Load())
}

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

	var providerCalls atomic.Int32
	mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		providerCalls.Add(1)
		_ = json.NewEncoder(w).Encode([]any{})
	}))
	defer mock.Close()

	fake := newFakeStore()
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/installations", nil)
	require.Equal(t, http.StatusBadRequest, resp.Code)
	require.Contains(t, resp.Body.String(), "GitHub App is not registered")
	require.Zero(t, providerCalls.Load())
}

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

	var providerCalls atomic.Int32
	mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		providerCalls.Add(1)
		require.Equal(t, "/app/installations", request.URL.Path)
		require.Equal(t, "100", request.URL.Query().Get("per_page"))
		require.Contains(t, request.Header.Get("Authorization"), "Bearer ")

		page := request.URL.Query().Get("page")
		if page == "2" {
			_ = json.NewEncoder(w).Encode([]map[string]any{{
				"id":          101,
				"app_id":      4242,
				"app_slug":    "agentteam-http",
				"target_type": "Organization",
				"permissions": githubManifestPermissions(),
				"events":      githubManifestEvents(),
				"account":     map[string]any{"login": "org-101", "id": 101},
			}})
			return
		}
		require.Equal(t, "1", page)
		items := make([]map[string]any, githubInstallationsPerPage)
		for index := range items {
			items[index] = map[string]any{
				"id":          index + 1,
				"app_id":      4242,
				"app_slug":    "agentteam-http",
				"target_type": "Organization",
				"permissions": githubManifestPermissions(),
				"events":      githubManifestEvents(),
				"account":     map[string]any{"login": fmt.Sprintf("org-%03d", index+1), "id": index + 1},
			}
		}
		_ = json.NewEncoder(w).Encode(items)
	}))
	defer mock.Close()

	fake := newFakeStore()
	fake.githubLoadConfig = provider.GitHubAppConfig{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		PrivateKeyPEM: generateTestPEM(t),
	}
	fake.githubLoadFound = true
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/installations", nil)
	require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
	var installations []map[string]any
	require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &installations))
	require.Len(t, installations, 101)
	require.Equal(t, "101", installations[100]["installationId"])
	require.Equal(t, int32(2), providerCalls.Load())
}

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

	var providerCalls atomic.Int32
	mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
		providerCalls.Add(1)
		page := request.URL.Query().Get("page")
		if page == "2" {
			http.Error(w, "provider detail must stay private", http.StatusBadGateway)
			return
		}
		require.Equal(t, "1", page)
		items := make([]map[string]any, githubInstallationsPerPage)
		for index := range items {
			items[index] = map[string]any{
				"id":          index + 1,
				"app_id":      4242,
				"app_slug":    "agentteam-http",
				"target_type": "Organization",
				"account":     map[string]any{"login": fmt.Sprintf("org-%03d", index+1), "id": index + 1},
			}
		}
		_ = json.NewEncoder(w).Encode(items)
	}))
	defer mock.Close()

	fake := newFakeStore()
	fake.githubLoadConfig = provider.GitHubAppConfig{
		AppID:         4242,
		AppSlug:       "agentteam-http",
		PrivateKeyPEM: generateTestPEM(t),
	}
	fake.githubLoadFound = true
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/installations", nil)
	require.Equal(t, http.StatusBadGateway, resp.Code)
	require.Equal(t, int32(2), providerCalls.Load())
	require.NotContains(t, resp.Body.String(), "org-001", "a later page failure must not return partial discovery data")
	require.NotContains(t, resp.Body.String(), "provider detail must stay private")
}

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

	mock := newGitHubMockServer(t)
	defer mock.Close()

	pemStr := generateTestPEM(t)
	fake := newFakeStore()
	fake.githubConnections = []provider.AppConnectionView{{ID: "cxn_1", ExternalSubjectRef: "789"}}
	fake.githubLoadConfig = provider.GitHubAppConfig{AppID: 4242, AppSlug: "agentteam-http", PrivateKeyPEM: pemStr}
	fake.githubLoadFound = true
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, mock.URL))

	resp := httputil.DoJSON(t, router, http.MethodGet, "/api/v1/workspaces/wsp_1/providers/github/connections", nil)
	require.Equal(t, http.StatusOK, resp.Code)

	var connections []provider.AppConnectionView
	require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &connections))
	require.Len(t, connections, 1)
}

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

	router := newProviderRouter(newGitHubTestDeps(t, newFakeStore(), nil, ""))
	resp := httputil.DoJSON(t, router, http.MethodDelete, "/api/v1/workspaces/wsp_1/providers/github/connections/%20", nil)
	require.Equal(t, http.StatusBadRequest, resp.Code)
}

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

	fake := newFakeStore()
	fake.githubConnections = []provider.AppConnectionView{{ID: "other"}}
	router := newProviderRouter(newGitHubTestDeps(t, fake, nil, ""))

	resp := httputil.DoJSON(t, router, http.MethodDelete, "/api/v1/workspaces/wsp_1/providers/github/connections/cxn_x", nil)
	require.Equal(t, http.StatusNotFound, resp.Code)
}

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

	fake := newFakeStore()
	fake.githubConnections = []provider.AppConnectionView{{ID: "cxn_1", ExternalSubjectRef: "789"}}
	blocker := &httputil.FakeBlocker{}
	router := newProviderRouter(newGitHubTestDeps(t, fake, blocker, ""))

	resp := httputil.DoJSON(t, router, http.MethodDelete, "/api/v1/workspaces/wsp_1/providers/github/connections/cxn_1", nil)
	require.Equal(t, http.StatusNoContent, resp.Code)
	require.Equal(t, 1, fake.githubDisablePrepareCalls)
	require.Equal(t, "cxn_1", fake.lastGithubDisable)
	require.Equal(t, fake.githubConnectionBase, fake.lastGithubDisableBase)
	require.Empty(t, blocker.BlockCalls, "GitHub connection disable owns subscription blocking inside its mutation transaction")
}

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

	fake := newFakeStore()
	fake.githubConnections = []provider.AppConnectionView{{ID: "cxn_1", ExternalSubjectRef: "789"}}
	fake.githubDisableErr = storeerr.ErrConflict
	blocker := &httputil.FakeBlocker{}
	router := newProviderRouter(newGitHubTestDeps(t, fake, blocker, ""))

	resp := httputil.DoJSON(t, router, http.MethodDelete, "/api/v1/workspaces/wsp_1/providers/github/connections/cxn_1", nil)
	require.Equal(t, http.StatusConflict, resp.Code)
	require.Empty(t, blocker.BlockCalls)
}
