package app

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/core/secrets"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceactor"
)

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()

	for index, claimant := range []string{"creator", "admin"} {
		t.Run(claimant, func(t *testing.T) {
			admin := seedAppAdminFixture(t, pool, "bot_initial_once_"+claimant, now.Add(time.Duration(index)*time.Minute))
			creator := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_initial_creator_"+claimant, now)
			peer := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_initial_peer_"+claimant, now)
			requested := requestRunnerAppOnboardingForTest(
				t,
				ctx,
				store,
				creator.UserID,
				admin.WorkspaceID,
				"app-initial-once-"+claimant,
			)
			require.Equal(t, AppInstallationRequestStatusPending, requested.Request.Status)
			require.Equal(t, 0, runnerCredentialCountForRequest(t, ctx, pool, requested.Request.ID))
			assertAppDefinitionHasNoCallbackSecret(t, ctx, pool, requested.Request.AppID)

			approved, err := store.ApproveAppInstallationRequestForUser(
				ctx,
				admin.UserID,
				admin.WorkspaceID,
				requested.Request.ID,
				DecideAppInstallationRequestParams{Reason: "approved"},
			)
			require.NoError(t, err)
			require.Equal(t, AppInstallationRequestStatusApproved, approved.Status)
			require.Equal(t, 0, runnerCredentialCountForRequest(t, ctx, pool, requested.Request.ID))
			assertAppDefinitionHasNoCallbackSecret(t, ctx, pool, requested.Request.AppID)

			for _, manager := range []appAdminFixture{creator, admin} {
				status, err := store.GetAppInitialCredentialsStatusForUser(
					ctx,
					manager.UserID,
					admin.WorkspaceID,
					approved.InstallationID,
				)
				require.NoError(t, err)
				require.Equal(t, AppInitialCredentialsStatusReadyToClaim, status.Status)
			}
			_, err = store.GetAppInitialCredentialsStatusForUser(ctx, peer.UserID, admin.WorkspaceID, approved.InstallationID)
			require.ErrorIs(t, err, ErrNotFound)

			claimantUserID := creator.UserID
			if claimant == "admin" {
				claimantUserID = admin.UserID
			}
			claimed, err := store.ClaimAppInitialCredentialsForUser(
				ctx,
				claimantUserID,
				admin.WorkspaceID,
				approved.InstallationID,
			)
			require.NoError(t, err)
			require.NotEmpty(t, claimed.RunnerToken)
			require.Empty(t, claimed.CallbackSigningSecret)
			authenticated, err := newRunnerServiceForTest(store).AuthenticateRunner(ctx, claimed.RunnerToken)
			require.NoError(t, err)
			require.Equal(t, approved.InstallationID, authenticated.ServiceInstallationID)
			require.Equal(t, 1, activeRunnerCredentialCountForInstallation(t, ctx, pool, approved.InstallationID))
			assertInitialCredentialRequestStoresNoPlaintext(t, ctx, pool, requested.Request.ID, claimed)

			for _, manager := range []appAdminFixture{creator, admin} {
				status, err := store.GetAppInitialCredentialsStatusForUser(
					ctx,
					manager.UserID,
					admin.WorkspaceID,
					approved.InstallationID,
				)
				require.NoError(t, err)
				require.Equal(t, AppInitialCredentialsStatusClaimed, status.Status)
				_, err = store.ClaimAppInitialCredentialsForUser(
					ctx,
					manager.UserID,
					admin.WorkspaceID,
					approved.InstallationID,
				)
				require.ErrorIs(t, err, ErrConflict)
			}
		})
	}
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	admin := seedAppAdminFixture(t, pool, "first_party_bot_initial_credentials", now)
	requester := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "first_party_bot_initial_requester", now)

	const preexistingProductSecret = "product-managed-callback-secret"
	mustExec(t, pool, `
UPDATE extension_definition_versions
SET manifest = $1,
    callback_url = 'https://product.example.test/agentteam/callback',
    updated_at = $2
WHERE service_definition_id = $3 AND version = '1.0.0'
`, []byte(`{
        "schema_version":"1",
        "slug":"github",
        "name":"GitHub",
        "version":"1.0.0",
        "callback":{"url":"https://product.example.test/agentteam/callback"},
        "scopes":["app.mentions.read","messages.write"],
        "capabilities":{"runtime":{
            "runtime":{"transports":["runner_stream","callback"],"default_transport":"runner_stream"},
            "events":["agentteam.message.mention"],
            "scopes":["app.mentions.read","messages.write"],
            "config_schema":{"type":"object","additionalProperties":false}
        }}
    }`), now, registry.GitHubAppID)
	mustExec(t, pool, `UPDATE service_definitions SET config = JSON_SET(COALESCE(config, JSON_OBJECT()), '$.callbackSigningSecret', $1) WHERE id = $2`, preexistingProductSecret, registry.GitHubAppID)

	_, err := store.FinalizeGitHubAppSetupForWorkspaceAdmin(
		ctx,
		admin.UserID,
		admin.WorkspaceID,
		validGitHubAppConfigParams(),
	)
	require.NoError(t, err)
	require.Zero(t, appInstallationRequestCountForWorkspace(t, ctx, pool, admin.WorkspaceID), "first-party setup must not create an external App credential request")
	installations, err := store.ListAppInstallationsForUser(ctx, admin.UserID, admin.WorkspaceID)
	require.NoError(t, err)
	require.Len(t, installations, 1)
	installation := installations[0]
	require.Equal(t, registry.GitHubAppID, installation.AppID)
	_, err = store.requireActiveAppRuntimeInstallation(ctx, admin.WorkspaceID, installation.ID, nil)
	require.NoError(t, err, "the fixture must genuinely be an active App runtime installation")
	require.Equal(t, preexistingProductSecret, loadAppDefinitionCallbackSecret(t, ctx, pool, registry.GitHubAppID), "approval must not rotate first-party callback ownership")
	for _, actor := range []appAdminFixture{admin, requester} {
		visibleInstallations, err := store.ListAppInstallationsForUser(ctx, actor.UserID, admin.WorkspaceID)
		require.NoError(t, err)
		found := false
		for _, visible := range visibleInstallations {
			if visible.ID != installation.ID {
				continue
			}
			found = true
			require.False(t, visible.CanManageApp, "product/first-party Apps never receive creator capability")
		}
		require.True(t, found)
	}

	adminStatus, err := store.GetAppInitialCredentialsStatusForUser(ctx, admin.UserID, admin.WorkspaceID, installation.ID)
	require.NoError(t, err)
	require.Equal(t, AppInitialCredentialsStatusNotApplicable, adminStatus.Status)
	_, err = store.ClaimAppInitialCredentialsForUser(ctx, admin.UserID, admin.WorkspaceID, installation.ID)
	require.ErrorIs(t, err, ErrForbidden)
	_, err = store.ClaimAppInitialCredentialsForUser(ctx, requester.UserID, admin.WorkspaceID, installation.ID)
	require.ErrorIs(t, err, ErrForbidden)
	_, err = store.CreateAppRunnerCredentialForUser(ctx, requester.UserID, admin.WorkspaceID, installation.ID, AppRunnerCredentialCreateParams{Name: "member must not manage product runtime"})
	require.ErrorIs(t, err, ErrForbidden)
	productRunner, err := store.CreateAppRunnerCredentialForUser(ctx, admin.UserID, admin.WorkspaceID, installation.ID, AppRunnerCredentialCreateParams{Name: "admin product recovery"})
	require.NoError(t, err, "first-party/product runtime management remains admin-only")
	require.NotEmpty(t, productRunner.Token)

	mustExec(t, pool, `UPDATE members SET workspace_role = 'admin', updated_at = $1 WHERE id = $2`, now.Add(time.Minute), requester.MemberID)
	promotedStatus, err := store.GetAppInitialCredentialsStatusForUser(ctx, requester.UserID, admin.WorkspaceID, installation.ID)
	require.NoError(t, err)
	require.Equal(t, AppInitialCredentialsStatusNotApplicable, promotedStatus.Status)
	_, err = store.ClaimAppInitialCredentialsForUser(ctx, requester.UserID, admin.WorkspaceID, installation.ID)
	require.ErrorIs(t, err, ErrForbidden, "promotion must not turn a first-party install request into external App credential authority")
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	admin := seedAppAdminFixture(t, pool, "missing_bot_review_snapshot", now)
	requester := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "missing_bot_review_requester", now)
	requested, err := store.RequestCustomAppInstallationForUser(ctx, requester.UserID, admin.WorkspaceID, directAppOnboardingParams("missing-review-snapshot-app"))
	require.NoError(t, err)

	// Approval requires the server-derived runtime review snapshot and must not
	// activate a callback installation on an empty HMAC key.
	mustExec(t, pool, `UPDATE service_installation_requests SET base_config = JSON_OBJECT() WHERE id = $1`, requested.Request.ID)
	_, err = store.ApproveAppInstallationRequestForUser(ctx, admin.UserID, admin.WorkspaceID, requested.Request.ID, DecideAppInstallationRequestParams{Reason: "must fail closed"})
	require.ErrorIs(t, err, ErrConflict)
	assertAppDefinitionHasNoCallbackSecret(t, ctx, pool, requested.Request.AppID)
	var activeInstallations int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_installations
WHERE workspace_id = $1
  AND service_definition_id = $2
  AND status = 'active'
`, admin.WorkspaceID, requested.Request.AppID).Scan(&activeInstallations))
	require.Zero(t, activeInstallations)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	admin := seedAppAdminFixture(t, pool, "bot_initial_terminal", now)
	requester := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_initial_terminal_requester", now)

	rejected, err := store.RequestCustomAppInstallationForUser(ctx, requester.UserID, admin.WorkspaceID, directAppOnboardingParams("app-initial-rejected"))
	require.NoError(t, err)
	rejectedDecision, err := store.RejectAppInstallationRequestForUser(ctx, admin.UserID, admin.WorkspaceID, rejected.Request.ID, DecideAppInstallationRequestParams{Reason: "no"})
	require.NoError(t, err)
	require.Equal(t, AppInstallationRequestStatusRejected, rejectedDecision.Status)
	assertTerminalAppRequestDeletesCatalogStateAndHasNoCredentials(t, ctx, pool, admin.WorkspaceID, rejected.Request.ID, rejected.Request.AppID)
	_, err = store.InstallAppForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, InstallAppParams{AppID: rejected.Request.AppID, Config: directAppOnboardingParams("ignored").Config})
	require.ErrorIs(t, err, ErrNotFound, "a rejected App no longer exists in Apps")

	withdrawn, err := store.RequestCustomAppInstallationForUser(ctx, requester.UserID, admin.WorkspaceID, directAppOnboardingParams("app-initial-withdrawn"))
	require.NoError(t, err)
	mustExec(t, pool, `UPDATE members SET workspace_role = 'admin', updated_at = NOW() WHERE id = $1`, requester.MemberID)
	withdrawnDecision, settled, err := store.SettlePrivilegedAppInstallationRequest(ctx, admin.WorkspaceID, withdrawn.Request.ID)
	require.NoError(t, err)
	require.True(t, settled)
	require.True(t, IsPrivilegedAppRequestWithdrawalDecision(withdrawnDecision))
	assertTerminalAppRequestDeletesCatalogStateAndHasNoCredentials(t, ctx, pool, admin.WorkspaceID, withdrawn.Request.ID, withdrawn.Request.AppID)
	_, err = store.InstallAppForWorkspaceAdmin(ctx, admin.UserID, admin.WorkspaceID, InstallAppParams{AppID: withdrawn.Request.AppID, Config: directAppOnboardingParams("ignored").Config})
	require.ErrorIs(t, err, ErrNotFound, "a withdrawn App no longer exists in Apps")
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	admin := seedAppAdminFixture(t, pool, "bot_onboarding_catalog", now)
	requester := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_onboarding_catalog_requester", now)
	peer := seedWorkspaceMemberFixture(t, pool, admin.WorkspaceID, "bot_onboarding_catalog_peer", now)

	const (
		requesterSlug = "app-onboarding-requester-visible"
		peerSlug      = "app-onboarding-peer-visible"
	)
	requesterPending := requestRunnerAppOnboardingForTest(t, ctx, store, requester.UserID, admin.WorkspaceID, requesterSlug)
	peerPending := requestRunnerAppOnboardingForTest(t, ctx, store, peer.UserID, admin.WorkspaceID, peerSlug)
	require.Equal(t, AppInstallationRequestStatusPending, requesterPending.Request.Status)
	require.Equal(t, AppInstallationRequestStatusPending, peerPending.Request.Status)
	_, err := store.RequestAppInstallationForUser(ctx, requester.UserID, admin.WorkspaceID, RequestAppInstallationParams{
		AppID: requesterPending.Request.AppID,
	})
	require.ErrorIs(t, err, ErrBadRequest, "workspace Apps must not enter the generic App install request path")
	_, err = store.RequestAppInstallationForUser(ctx, peer.UserID, admin.WorkspaceID, RequestAppInstallationParams{
		AppID: requesterPending.Request.AppID,
	})
	require.ErrorIs(t, err, ErrNotFound, "a peer who retained a hidden App id must not discover it")
	var requesterRequestCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_installation_requests
WHERE workspace_id = $1
  AND service_definition_id = $2
`, admin.WorkspaceID, requesterPending.Request.AppID).Scan(&requesterRequestCount))
	require.Equal(t, 1, requesterRequestCount)

	assertWorkspaceCatalogContainsMentionApp(t, ctx, store, requester.UserID, admin.WorkspaceID, requesterSlug)
	assertWorkspaceCatalogOmitsApp(t, ctx, store, requester.UserID, admin.WorkspaceID, peerSlug)
	assertWorkspaceCatalogContainsMentionApp(t, ctx, store, peer.UserID, admin.WorkspaceID, peerSlug)
	assertWorkspaceCatalogOmitsApp(t, ctx, store, peer.UserID, admin.WorkspaceID, requesterSlug)
	assertWorkspaceCatalogContainsMentionApp(t, ctx, store, admin.UserID, admin.WorkspaceID, requesterSlug)
	assertWorkspaceCatalogContainsMentionApp(t, ctx, store, admin.UserID, admin.WorkspaceID, peerSlug)
	assertWorkspaceCatalogContainsProductApps(t, ctx, pool, store, requester.UserID, admin.WorkspaceID)
	assertWorkspaceCatalogContainsProductApps(t, ctx, pool, store, peer.UserID, admin.WorkspaceID)

	requesterApproved, err := store.ApproveAppInstallationRequestForUser(ctx, admin.UserID, admin.WorkspaceID, requesterPending.Request.ID, DecideAppInstallationRequestParams{Reason: "approved requester App"})
	require.NoError(t, err)
	peerApproved, err := store.ApproveAppInstallationRequestForUser(ctx, admin.UserID, admin.WorkspaceID, peerPending.Request.ID, DecideAppInstallationRequestParams{Reason: "approved peer App"})
	require.NoError(t, err)

	assertWorkspaceCatalogContainsMentionApp(t, ctx, store, requester.UserID, admin.WorkspaceID, requesterSlug)
	assertWorkspaceCatalogOmitsApp(t, ctx, store, requester.UserID, admin.WorkspaceID, peerSlug)
	assertWorkspaceCatalogContainsMentionApp(t, ctx, store, peer.UserID, admin.WorkspaceID, peerSlug)
	assertWorkspaceCatalogOmitsApp(t, ctx, store, peer.UserID, admin.WorkspaceID, requesterSlug)
	assertWorkspaceCatalogContainsMentionApp(t, ctx, store, admin.UserID, admin.WorkspaceID, requesterSlug)
	assertWorkspaceCatalogContainsMentionApp(t, ctx, store, admin.UserID, admin.WorkspaceID, peerSlug)

	assertWorkspaceInstallationIDs(t, ctx, store, requester.UserID, admin.WorkspaceID, requesterApproved.InstallationID)
	assertWorkspaceInstallationIDs(t, ctx, store, peer.UserID, admin.WorkspaceID, peerApproved.InstallationID)
	assertWorkspaceInstallationIDs(t, ctx, store, admin.UserID, admin.WorkspaceID, requesterApproved.InstallationID, peerApproved.InstallationID)
}

func assertWorkspaceCatalogContainsMentionApp(t *testing.T, ctx context.Context, store *Store, userID string, workspaceID string, slug string) {
	t.Helper()
	apps, err := store.ListAppsForUser(ctx, userID, workspaceID)
	require.NoError(t, err)
	for _, app := range apps {
		if app.Slug != slug {
			continue
		}
		require.NotNil(t, app.RuntimeCapability)
		require.True(t, app.RuntimeCapability.Supported)
		require.Contains(t, app.RuntimeCapability.Events, registry.AppEventMessageMention)
		return
	}
	t.Fatalf("workspace App catalog does not contain onboarding App %q", slug)
}

func assertWorkspaceCatalogOmitsApp(t *testing.T, ctx context.Context, store *Store, userID string, workspaceID string, slug string) {
	t.Helper()
	apps, err := store.ListAppsForUser(ctx, userID, workspaceID)
	require.NoError(t, err)
	for _, app := range apps {
		require.NotEqual(t, slug, app.Slug)
	}
}

func assertWorkspaceCatalogContainsProductApps(t *testing.T, ctx context.Context, pool coredb.DB, store *Store, userID string, workspaceID string) {
	t.Helper()
	apps, err := store.ListAppsForUser(ctx, userID, workspaceID)
	require.NoError(t, err)
	productSlugs := make([]string, 0, len(apps))
	for _, app := range apps {
		if registry.IsProductApp(app.ID, app.Slug) {
			productSlugs = append(productSlugs, app.Slug)
		}
	}
	// The seeded database is the ground truth for the product App catalog;
	// it is independent of the in-code registry map, so a registry deletion
	// or a seeding/visibility gap cannot pass silently on both sides.
	seededSlugs := make([]string, 0, len(productSlugs))
	rows, err := pool.Query(ctx,
		`SELECT slug FROM service_definitions WHERE service_kind = 'app' AND JSON_UNQUOTE(JSON_EXTRACT(COALESCE(config, JSON_OBJECT()), '$.appKind')) = 'first_party' AND visibility = 'public'`)
	require.NoError(t, err)
	defer rows.Close()
	for rows.Next() {
		var slug string
		require.NoError(t, rows.Scan(&slug))
		seededSlugs = append(seededSlugs, slug)
	}
	require.NoError(t, rows.Err())
	require.ElementsMatch(t, seededSlugs, productSlugs,
		"every seeded public first-party App must be user-visible as a product App")
	require.ElementsMatch(t, seededSlugs, registry.ProductAppSlugs(),
		"the registry must classify exactly the seeded public first-party Apps")
}

func assertWorkspaceInstallationIDs(t *testing.T, ctx context.Context, store *Store, userID string, workspaceID string, expected ...string) {
	t.Helper()
	installations, err := store.ListAppInstallationsForUser(ctx, userID, workspaceID)
	require.NoError(t, err)
	ids := make([]string, 0, len(installations))
	for _, installation := range installations {
		ids = append(ids, installation.ID)
	}
	require.ElementsMatch(t, expected, ids)
}

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

	ctx := context.Background()
	pool := startAppRollbackDB(t, ctx)
	store := New(pool, Options{AppCredentialStorageKey: testAppCredentialStorageKey()})
	now := time.Now().UTC()
	workspaceA := seedAppAdminFixture(t, pool, "bot_onboarding_isolation_a", now)
	requesterA := seedWorkspaceMemberFixture(t, pool, workspaceA.WorkspaceID, "bot_onboarding_isolation_requester_a", now)
	workspaceB := seedAppAdminFixture(t, pool, "bot_onboarding_isolation_b", now)
	requesterB := seedWorkspaceMemberFixture(t, pool, workspaceB.WorkspaceID, "bot_onboarding_isolation_requester_b", now)

	const slug = "workspace-isolated-app"
	created := requestRunnerAppOnboardingForTest(t, ctx, store, requesterA.UserID, workspaceA.WorkspaceID, slug)
	require.Equal(t, AppInstallationRequestStatusPending, created.Request.Status)

	for name, params := range map[string]RequestAppInstallationParams{
		"app id":   {AppID: created.Request.AppID},
		"app slug": {AppSlug: slug},
	} {
		t.Run(name, func(t *testing.T) {
			_, err := store.RequestAppInstallationForUser(ctx, requesterB.UserID, workspaceB.WorkspaceID, params)
			require.ErrorIs(t, err, ErrNotFound)
		})
	}

	_, err := store.RequestCustomAppInstallationForUser(ctx, requesterB.UserID, workspaceB.WorkspaceID, RequestCustomAppInstallationParams{
		Slug:     slug,
		Name:     slug,
		Manifest: runnerAppManifestForTest(slug),
	})
	require.ErrorIs(t, err, ErrConflict, "narrow onboarding must not associate an existing unlisted App with a second workspace")

	var workspaceBRequestCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_installation_requests
WHERE workspace_id = $1
  AND service_definition_id = $2
`, workspaceB.WorkspaceID, created.Request.AppID).Scan(&workspaceBRequestCount))
	require.Zero(t, workspaceBRequestCount)
}

func requestRunnerAppOnboardingForTest(
	t *testing.T,
	ctx context.Context,
	store *Store,
	requesterUserID string,
	workspaceID string,
	slug string,
) CustomAppRequestResult {
	t.Helper()
	result, err := store.RequestCustomAppInstallationForUser(ctx, requesterUserID, workspaceID, RequestCustomAppInstallationParams{
		Slug:     slug,
		Name:     slug,
		Manifest: runnerAppManifestForTest(slug),
	})
	require.NoError(t, err)
	return result
}

func runnerAppManifestForTest(slug string) map[string]any {
	return map[string]any{
		"schema_version": "1",
		"slug":           slug,
		"name":           slug,
		"version":        "1.0.0",
		"scopes":         []string{AppMentionScopeRead, AppRuntimeScopeMessageWrite},
		"capabilities": map[string]any{
			"runtime": map[string]any{
				"runtime": map[string]any{
					"transports":        []string{serviceactor.RuntimeTransportKindRunnerStream},
					"default_transport": serviceactor.RuntimeTransportKindRunnerStream,
				},
				"events": []string{registry.AppEventMessageMention},
				"scopes": []string{AppMentionScopeRead, AppRuntimeScopeMessageWrite},
				"config_schema": map[string]any{
					"type":                 "object",
					"additionalProperties": true,
				},
			},
		},
	}
}

func runnerCredentialCountForRequest(t *testing.T, ctx context.Context, pool coredb.QueryRower, requestID string) int {
	t.Helper()
	var count int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_runtime_credentials credential
JOIN service_installation_requests request
  ON request.service_installation_id = credential.service_installation_id
WHERE request.id = $1
`, requestID).Scan(&count))
	return count
}

func activeRunnerCredentialCountForInstallation(t *testing.T, ctx context.Context, pool coredb.QueryRower, installationID string) int {
	t.Helper()
	var count int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_runtime_credentials
WHERE service_installation_id = $1
  AND credential_kind = $2
  AND transport_kind = $3
  AND status = $4
`, installationID, serviceactor.RuntimeCredentialKindRunnerStreamToken,
		serviceactor.RuntimeTransportKindRunnerStream, serviceactor.RuntimeCredentialStatusActive).Scan(&count))
	return count
}

func assertAppDefinitionHasNoCallbackSecret(t *testing.T, ctx context.Context, pool coredb.QueryRower, appID string) {
	t.Helper()
	require.Empty(t, loadAppDefinitionCallbackSecret(t, ctx, pool, appID))
}

func loadAppDefinitionCallbackSecret(t *testing.T, ctx context.Context, pool coredb.QueryRower, appID string) string {
	t.Helper()
	var secret string
	require.NoError(t, pool.QueryRow(ctx, `SELECT COALESCE(JSON_UNQUOTE(JSON_EXTRACT(COALESCE(config, JSON_OBJECT()), '$.callbackSigningSecret')), '') FROM service_definitions WHERE id = $1`, appID).Scan(&secret))
	return secret
}

func assertInitialCredentialRequestStoresNoPlaintext(
	t *testing.T,
	ctx context.Context,
	pool coredb.QueryRower,
	requestID string,
	claimed AppInitialCredentialsClaimResult,
) {
	t.Helper()
	var baseConfig string
	var requestedConfig string
	var claimedCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT base_config,
       requested_config,
       CASE WHEN initial_credentials_claimed_at IS NULL THEN 0 ELSE 1 END
FROM service_installation_requests
WHERE id = $1
	`, requestID).Scan(&baseConfig, &requestedConfig, &claimedCount))
	require.Equal(t, 1, claimedCount)
	for _, plaintext := range []string{claimed.CallbackSigningSecret, claimed.RunnerToken} {
		if plaintext == "" {
			continue
		}
		require.NotContains(t, baseConfig, plaintext)
		require.NotContains(t, requestedConfig, plaintext)
	}

	var runnerHash string
	var runnerHint string
	var metadata string
	require.NoError(t, pool.QueryRow(ctx, `
SELECT credential.token_hash, credential.token_hint, credential.metadata
FROM service_runtime_credentials credential
JOIN service_installation_requests request
  ON request.service_installation_id = credential.service_installation_id
WHERE request.id = $1
  AND credential.credential_kind = $2
  AND credential.transport_kind = $3
  AND credential.status = $4
LIMIT 1
`, requestID, serviceactor.RuntimeCredentialKindRunnerStreamToken,
		serviceactor.RuntimeTransportKindRunnerStream, serviceactor.RuntimeCredentialStatusActive).Scan(
		&runnerHash,
		&runnerHint,
		&metadata,
	))
	require.Equal(t, secrets.HashToken(claimed.RunnerToken), runnerHash)
	require.NotEqual(t, claimed.RunnerToken, runnerHash)
	require.NotEmpty(t, runnerHint)
	require.NotContains(t, metadata, claimed.RunnerToken)
}

func assertTerminalAppRequestDeletesCatalogStateAndHasNoCredentials(t *testing.T, ctx context.Context, pool coredb.QueryRower, workspaceID string, requestID string, appID string) {
	t.Helper()
	var versionCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM extension_definition_versions WHERE service_definition_id = $1`, appID).Scan(&versionCount))
	require.Zero(t, versionCount, "App versions must not survive terminal App onboarding")
	var attributionCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM extension_catalog_attributions WHERE workspace_id = $1 AND service_definition_id = $2`, workspaceID, appID).Scan(&attributionCount))
	require.Zero(t, attributionCount, "workspace attribution must not survive terminal App onboarding")
	var auditDefinitionCount int
	require.NoError(t, pool.QueryRow(ctx, `SELECT COUNT(*) FROM service_definitions WHERE id = $1`, appID).Scan(&auditDefinitionCount))
	require.Equal(t, 1, auditDefinitionCount, "the service-definition snapshot remains for Approval audit projection")
	require.Equal(t, 0, runnerCredentialCountForRequest(t, ctx, pool, requestID))
	var credentialStateCount int
	require.NoError(t, pool.QueryRow(ctx, `
SELECT COUNT(*)
FROM service_installation_requests
WHERE id = $1
  AND initial_credentials_claimed_at IS NOT NULL
`, requestID).Scan(&credentialStateCount))
	require.Zero(t, credentialStateCount)
}
