package approvalapp

import (
	"encoding/json"
	"fmt"
	"github.com/tidbcloud/AgentTeam/internal/store/app/registry"
	"testing"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/service/serviceaccount"
	"github.com/tidbcloud/AgentTeam/internal/service/team/roleproposal"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	serviceaccountstore "github.com/tidbcloud/AgentTeam/internal/store/serviceaccount"
)

type approvalCardDocument struct {
	Template string           `json:"template"`
	Title    string           `json:"title"`
	TitleKey string           `json:"titleKey"`
	Status   string           `json:"status"`
	Text     string           `json:"text"`
	TextKey  string           `json:"textKey"`
	Request  map[string]any   `json:"request"`
	Approval map[string]any   `json:"approval"`
	Fields   []map[string]any `json:"fields"`
	Actions  []map[string]any `json:"actions"`
}

func TestTeamRoleProposalCardsRenderDedicatedDiffAndCreatorOnlyActions(t *testing.T) {
	payload := teamRoleProposalPayload{}
	payload.Base.Team.Description = "Coordinate deployments"
	payload.Base.Members = append(payload.Base.Members, struct {
		TeamMemberID      string `json:"teamMemberId"`
		MemberName        string `json:"memberName,omitempty"`
		RoleLabel         string `json:"roleLabel"`
		RoleOverlayPrompt string `json:"roleOverlayPrompt"`
		WakeOnTeamMention bool   `json:"wakeOnTeamMention"`
	}{
		TeamMemberID: "wtm_release_lead", MemberName: "Release Lead at submission",
		RoleLabel: "Release owner", RoleOverlayPrompt: "Own deployments", WakeOnTeamMention: true,
	})
	payload.Proposed.Description = approvalAppString("Coordinate releases")
	payload.Proposed.Members = append(payload.Proposed.Members, struct {
		TeamMemberID      string  `json:"teamMemberId"`
		MemberName        string  `json:"memberName,omitempty"`
		RoleLabel         *string `json:"roleLabel,omitempty"`
		RoleOverlayPrompt *string `json:"roleOverlayPrompt,omitempty"`
		WakeOnTeamMention *bool   `json:"wakeOnTeamMention,omitempty"`
	}{
		TeamMemberID: "wtm_release_lead", MemberName: "Release Lead at submission",
		RoleLabel:         approvalAppString("Release reviewer"),
		RoleOverlayPrompt: approvalAppString("Own the release plan"),
		WakeOnTeamMention: approvalAppBool(false),
	})
	proposal := roleproposal.Proposal{ID: "appr_release", TeamID: "team_release"}
	instance := approval.Instance{
		ID: "appr_release", SubjectType: SubjectTypeTeamRoleProposal,
		SubjectRef: proposal.TeamID, State: approval.StatePending, Version: 1,
	}
	assignment := approval.Assignment{
		ID: "apasg_release", ApprovalInstanceID: instance.ID,
		ApproverMemberID: "mem_team_creator", Status: approval.AssignmentPending,
	}

	pending, err := pendingTeamRoleProposalCardContent(proposal, instance, assignment, payload, "Team Owner")
	require.NoError(t, err)
	document := decodeApprovalCard(t, pending)
	require.Equal(t, "approval_diff", document.Template)
	require.Equal(t, "team_role_proposal_review", document.TitleKey)
	require.Equal(t, approval.StatePending, document.Status)
	require.Len(t, document.Actions, 2)
	require.Equal(t, "暂不通过", document.Actions[1]["label"])
	for _, action := range document.Actions {
		require.Equal(t, map[string]any{
			"kind": "members", "memberIds": []any{"mem_team_creator"},
		}, action["actorPolicy"])
	}
	var raw map[string]any
	require.NoError(t, json.Unmarshal(pending.CardJSON, &raw))
	diff := raw["diff"].(map[string]any)
	require.Equal(t, "Coordinate deployments", diff["description"].(map[string]any)["before"])
	require.Equal(t, "Coordinate releases", diff["description"].(map[string]any)["after"])
	require.NotContains(t, diff, "teamPrompt")
	require.Len(t, diff["members"], 1)
	member := diff["members"].([]any)[0].(map[string]any)
	require.Equal(t, "Release Lead at submission", member["memberName"])
	require.Equal(t, false, member["wakeOnTeamMention"].(map[string]any)["after"])
	rejectInputSchema := raw["actions"].([]any)[1].(map[string]any)["inputSchema"].(map[string]any)
	require.False(t, rejectInputSchema["additionalProperties"].(bool))
	localeSchema := rejectInputSchema["properties"].(map[string]any)["locale"].(map[string]any)
	require.Equal(t, []any{"en", "zh-CN", "ja"}, localeSchema["enum"])

	instance.State = approval.StateApproved
	instance.Version = 2
	resolved, err := resolvedTeamRoleProposalCardContent(proposal, instance, payload, "Team Owner")
	require.NoError(t, err)
	resolvedDocument := decodeApprovalCard(t, resolved)
	require.Equal(t, "approval_diff", resolvedDocument.Template)
	require.Equal(t, "team_role_proposal_approved", resolvedDocument.TitleKey)
	require.Empty(t, resolvedDocument.Actions)
}

func TestStaleTeamRoleProposalCardKeepsRemovedMemberSnapshotName(t *testing.T) {
	payload := teamRoleProposalPayload{}
	payload.Base.Members = append(payload.Base.Members, struct {
		TeamMemberID      string `json:"teamMemberId"`
		MemberName        string `json:"memberName,omitempty"`
		RoleLabel         string `json:"roleLabel"`
		RoleOverlayPrompt string `json:"roleOverlayPrompt"`
		WakeOnTeamMention bool   `json:"wakeOnTeamMention"`
	}{TeamMemberID: "wtm_removed", MemberName: "Removed Release Agent", RoleOverlayPrompt: "Old responsibility"})
	payload.Proposed.Members = append(payload.Proposed.Members, struct {
		TeamMemberID      string  `json:"teamMemberId"`
		MemberName        string  `json:"memberName,omitempty"`
		RoleLabel         *string `json:"roleLabel,omitempty"`
		RoleOverlayPrompt *string `json:"roleOverlayPrompt,omitempty"`
		WakeOnTeamMention *bool   `json:"wakeOnTeamMention,omitempty"`
	}{TeamMemberID: "wtm_removed", MemberName: "Removed Release Agent", RoleOverlayPrompt: approvalAppString("New responsibility")})
	instance := approval.Instance{ID: "appr_stale_removed", State: approval.StateCancelled, Version: 2, DecisionReason: "stale_subject"}

	content, err := resolvedTeamRoleProposalCardContent(roleproposal.Proposal{ID: instance.ID, TeamID: "team_history"}, instance, payload, "Team Owner")
	require.NoError(t, err)
	var card map[string]any
	require.NoError(t, json.Unmarshal(content.CardJSON, &card))
	require.Equal(t, "team_role_proposal_stale", card["titleKey"])
	member := card["diff"].(map[string]any)["members"].([]any)[0].(map[string]any)
	require.Equal(t, "Removed Release Agent", member["memberName"])
}

func approvalAppString(value string) *string { return &value }
func approvalAppBool(value bool) *bool       { return &value }

func TestTerminalTeamRoleProposalActorUsesStrictIDDomains(t *testing.T) {
	var payload teamRoleProposalPayload
	payload.Applicant.AgentID = "agt_release_leader"

	require.Equal(t, app.AppNotificationActorRef{
		Type: "agent", ID: "agt_release_leader",
	}, terminalTeamRoleProposalActor(approval.Instance{
		State: approval.StateCancelled, DecisionReason: "withdrawn_by_leader",
		DecidedByMemberID: "mem_release_leader",
	}, payload))
	require.Equal(t, app.AppNotificationActorRef{
		Type: "human", ID: "mem_team_owner",
	}, terminalTeamRoleProposalActor(approval.Instance{
		State: approval.StateApproved, DecidedByMemberID: "mem_team_owner",
	}, payload))
	require.Equal(t, app.AppNotificationActorRef{
		Type: "human", ID: "mem_team_owner",
	}, terminalTeamRoleProposalActor(approval.Instance{
		State: approval.StateCancelled, DecisionReason: "stale_subject",
		DecidedByMemberID: "mem_team_owner",
	}, payload))
}

func decodeApprovalCard(t testing.TB, content app.AppNotificationContent) approvalCardDocument {
	t.Helper()
	var document approvalCardDocument
	require.NoError(t, json.Unmarshal(content.CardJSON, &document))
	return document
}

func TestActiveAppInstallationReviewerProjectionRequiresActiveInteractiveCard(t *testing.T) {
	reviewerBasis := RecipientBasisAppInstallationReviewer
	reviewerID := "mem_reviewer"
	otherReviewerID := "mem_other"
	pendingVersion := int64(1)
	notifications := []app.AppNotificationRecord{
		{
			ID: "resolved_interactive", ContentKind: app.AppNotificationContentInteractiveCard,
			Status:             app.AppNotificationStatusResolved,
			RecipientBasisKind: &reviewerBasis, RecipientBasisRef: &reviewerID,
		},
		{
			ID: "active_message", ContentKind: app.AppNotificationContentMessage,
			Status:             app.AppNotificationStatusActive,
			RecipientBasisKind: &reviewerBasis, RecipientBasisRef: &reviewerID,
		},
		{
			ID: "other_active_interactive", ContentKind: app.AppNotificationContentInteractiveCard,
			Status: app.AppNotificationStatusActive, BusinessObjectVersion: &pendingVersion,
			RecipientBasisKind: &reviewerBasis, RecipientBasisRef: &otherReviewerID,
		},
	}

	_, found, historyCount := activeAppInstallationReviewerProjection(notifications, reviewerID)
	require.False(t, found)
	require.Equal(t, 2, historyCount)

	notifications = append(notifications, app.AppNotificationRecord{
		ID: "current_active_interactive", ContentKind: app.AppNotificationContentInteractiveCard,
		Status: app.AppNotificationStatusActive, BusinessObjectVersion: &pendingVersion,
		RecipientBasisKind: &reviewerBasis, RecipientBasisRef: &reviewerID,
	})
	active, found, historyCount := activeAppInstallationReviewerProjection(notifications, reviewerID)
	require.True(t, found)
	require.Equal(t, "current_active_interactive", active.ID)
	require.Equal(t, 3, historyCount)
}

func requireHumanApprovalRequest(t testing.TB, request map[string]any, name, description string) {
	t.Helper()
	require.Equal(t, name, request["name"])
	require.Equal(t, description, request["description"])
	require.Equal(t, "Alice Chen", request["applicantName"])
	require.NotContains(t, request, "handle")
	require.NotContains(t, request, "appSlug")
	require.NotContains(t, request, "installationId")
	require.NotContains(t, request, "applicantMemberId")
}

func TestServiceAccountApprovalCardsShowNameAndApplicant(t *testing.T) {
	request := serviceaccountstore.CreateRequest{ID: "sacreq_1"}
	payload := serviceaccount.WorkspaceCreatePayload{
		Name: "Release News", Description: "Release notes and maintenance windows",
	}
	instance := approval.Instance{ID: "appr_1", Version: 1}
	assignment := approval.Assignment{ID: "assign_1"}

	pending, err := pendingCardContent(request, instance, assignment, payload, "Alice Chen", []string{"Workspace Owner"})
	require.NoError(t, err)
	pendingDocument := decodeApprovalCard(t, pending)
	require.Equal(t, "service_account_create_review", pendingDocument.TitleKey)
	require.Equal(t, approval.StatePending, pendingDocument.Status)
	require.Equal(t, []any{"Workspace Owner"}, pendingDocument.Approval["currentProcessorNames"])
	requireHumanApprovalRequest(
		t,
		pendingDocument.Request,
		"Release News",
		"Release notes and maintenance windows",
	)

	applicantPending, err := pendingApplicantCardContent(request, instance, payload, "Alice Chen", []string{"Workspace Owner"})
	require.NoError(t, err)
	applicantPendingDocument := decodeApprovalCard(t, applicantPending)
	require.Equal(t, "服务号创建申请已提交", applicantPendingDocument.Title)
	require.Equal(t, "service_account_create_submitted", applicantPendingDocument.TitleKey)
	require.Equal(t, approval.StatePending, applicantPendingDocument.Status)
	require.Empty(t, applicantPendingDocument.Actions)
	require.Equal(t, []any{"Workspace Owner"}, applicantPendingDocument.Approval["currentProcessorNames"])
	requireHumanApprovalRequest(
		t,
		applicantPendingDocument.Request,
		"Release News",
		"Release notes and maintenance windows",
	)

	instance.State = approval.StateApproved
	instance.DecidedByMemberID = "mem_owner"
	resolved, err := resolvedCardContent(
		request,
		instance,
		payload,
		"Alice Chen",
		"Workspace Owner",
	)
	require.NoError(t, err)
	document := decodeApprovalCard(t, resolved)
	require.Empty(t, document.Template)
	require.Equal(t, "service_account_create_approved", document.TitleKey)
	require.Equal(t, approval.StateApproved, document.Status)
	require.Empty(t, document.Actions)
	requireHumanApprovalRequest(
		t,
		document.Request,
		"Release News",
		"Release notes and maintenance windows",
	)
	require.Equal(t, approval.StateApproved, document.Approval["state"])
	require.Equal(t, "Workspace Owner", document.Approval["processorName"])
	require.NotContains(t, document.Approval, "decidedByMemberId")

	instance.State = approval.StateRejected
	rejected, err := resolvedCardContent(
		request,
		instance,
		payload,
		"Alice Chen",
		"Workspace Owner",
	)
	require.NoError(t, err)
	require.Equal(t, "service_account_create_rejected", decodeApprovalCard(t, rejected).TitleKey)

	result, err := applicantResultCardContent(
		request,
		instance,
		payload,
		"Workspace Owner",
	)
	require.NoError(t, err)
	resultDocument := decodeApprovalCard(t, result)
	require.Equal(t, "approval_result", resultDocument.Template)
	require.Equal(t, "你的服务号创建申请已拒绝", resultDocument.Title)
	require.Equal(t, "service_account_create_result_rejected", resultDocument.TitleKey)
	require.Equal(t, approval.StateRejected, resultDocument.Status)
	require.Empty(t, resultDocument.Actions)
	require.Equal(t, "Release News", resultDocument.Request["name"])
	require.NotContains(t, resultDocument.Request, "applicantName")
	require.NotContains(t, resultDocument.Request, "handle")
	require.NotContains(t, resultDocument.Approval, "decidedByMemberId")
}

func TestAppInstallationApprovalCardsShowExactManifestConfigAndHideInternalRouting(t *testing.T) {
	request := app.AppInstallationRequestView{
		ID:                     "svcinstreq_1",
		AppName:                "Release App",
		AppSlug:                "release-app",
		RequestKind:            app.AppInstallationRequestKindInstall,
		RequestedByMemberID:    "mem_applicant",
		RequestedByDisplayName: "Alice Chen",
		RequestedConfig: map[string]any{
			"channel": "release-updates",
			"limits":  map[string]any{"daily": float64(10)},
		},
		RequestedScopes: []string{"messages.write", "messages.read", "messages.write"},
		RequestReason:   "Release automation",
		Status:          app.AppInstallationRequestStatusPending,
	}

	pending, err := pendingAppInstallationCardContent(request)
	require.NoError(t, err)
	pendingDocument := decodeApprovalCard(t, pending)
	require.Equal(t, "app_installation_review", pendingDocument.TitleKey)
	require.Equal(t, "审批 App 安装申请", pendingDocument.Title)
	require.Equal(t, app.AppInstallationRequestStatusPending, pendingDocument.Status)
	require.Equal(t, "Release App", pendingDocument.Request["appName"])
	require.Equal(t, app.AppInstallationRequestKindInstall, pendingDocument.Request["operation"])
	require.Equal(t, map[string]any{}, pendingDocument.Request["baseConfig"])
	require.Equal(t, request.RequestedConfig, pendingDocument.Request["requestedConfig"])
	require.Equal(t, "Alice Chen", pendingDocument.Request["applicantName"])
	require.NotContains(t, pendingDocument.Request, "requestedScopes")
	require.Equal(t, []map[string]any{
		{"label": "operation", "value": app.AppInstallationRequestKindInstall},
		{"label": "base_config", "value": "{}"},
		{
			"label": "requested_config",
			"value": "{\n  \"channel\": \"release-updates\",\n  \"limits\": {\n    \"daily\": 10\n  }\n}",
		},
	}, pendingDocument.Fields)
	require.NotContains(t, pendingDocument.Request, "appSlug")
	require.NotContains(t, pendingDocument.Request, "installationId")
	require.NotContains(t, pendingDocument.Request, "applicantMemberId")
	require.NotContains(t, pending.FallbackText, "release-app")

	request.Status = app.AppInstallationRequestStatusApproved
	request.DecidedByMemberID = "mem_owner"
	request.InstallationID = "appinst_internal"
	resolved, err := resolvedAppInstallationCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	resolvedDocument := decodeApprovalCard(t, resolved)
	require.Empty(t, resolvedDocument.Template)
	require.Equal(t, "app_installation_approved", resolvedDocument.TitleKey)
	require.Equal(t, app.AppInstallationRequestStatusApproved, resolvedDocument.Status)
	require.Equal(t, "Release App", resolvedDocument.Request["appName"])
	require.Equal(t, "Alice Chen", resolvedDocument.Request["applicantName"])
	require.Equal(t, "Release automation", resolvedDocument.Request["reason"])
	require.NotContains(t, resolvedDocument.Request, "requestedScopes")
	require.NotContains(t, resolvedDocument.Request, "appSlug")
	require.NotContains(t, resolvedDocument.Request, "installationId")
	require.Equal(t, app.AppInstallationRequestStatusApproved, resolvedDocument.Approval["state"])
	require.Equal(t, "Workspace Owner", resolvedDocument.Approval["processorName"])
	require.NotContains(t, resolvedDocument.Approval, "decidedByMemberId")

	approvedResult, err := appInstallationApplicantResultCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	approvedResultDocument := decodeApprovalCard(t, approvedResult)
	require.Equal(t, "approval_result", approvedResultDocument.Template)
	require.Equal(t, "app_installation_result_approved", approvedResultDocument.TitleKey)
	require.Len(t, approvedResultDocument.Actions, 1)
	require.Equal(t, "open_app", approvedResultDocument.Actions[0]["actionKey"])
	require.Equal(t, "open_url", approvedResultDocument.Actions[0]["type"])
	require.Equal(t, "navigate", approvedResultDocument.Actions[0]["semantics"])
	require.Equal(t, "/apps/release-app", approvedResultDocument.Actions[0]["url"])
	require.NotContains(t, approvedResultDocument.Request, "appSlug")
	require.NotContains(t, approvedResultDocument.Request, "installationId")
	require.NotContains(t, approvedResultDocument.Request, "requestedScopes")
	require.NotContains(t, approvedResult.FallbackText, "runtime")

	request.Status = app.AppInstallationRequestStatusRejected
	rejected, err := resolvedAppInstallationCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	require.Equal(t, "app_installation_rejected", decodeApprovalCard(t, rejected).TitleKey)

	result, err := appInstallationApplicantResultCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	resultDocument := decodeApprovalCard(t, result)
	require.Equal(t, "approval_result", resultDocument.Template)
	require.Equal(t, "你的 App 安装申请已拒绝", resultDocument.Title)
	require.Equal(t, "app_installation_result_rejected", resultDocument.TitleKey)
	require.Equal(t, app.AppInstallationRequestStatusRejected, resultDocument.Status)
	require.Empty(t, resultDocument.Actions)
	require.Equal(t, "Release App", resultDocument.Request["appName"])
	require.NotContains(t, resultDocument.Request, "applicantName")
	require.NotContains(t, resultDocument.Request, "appSlug")
	require.NotContains(t, resultDocument.Request, "installationId")
	require.NotContains(t, resultDocument.Approval, "decidedByMemberId")
}

func TestAppInstallationApplicantReceiptIsMinimalForEveryRequestKind(t *testing.T) {
	testCases := []struct {
		name           string
		requestKind    string
		appID          string
		appName        string
		appSlug        string
		requestTitle   string
		titleKeyPrefix string
	}{
		{
			name:           "install",
			requestKind:    app.AppInstallationRequestKindInstall,
			appName:        "Release App",
			appSlug:        "release-app",
			requestTitle:   "应用集成创建申请",
			titleKeyPrefix: "apps_integration_creation",
		},
		{
			name:           "configure",
			requestKind:    app.AppInstallationRequestKindConfigure,
			appName:        "Release App",
			appSlug:        "release-app",
			requestTitle:   "App 配置变更申请",
			titleKeyPrefix: "app_configuration",
		},
		{
			name:           "gitea_connect",
			requestKind:    app.AppInstallationRequestKindConnect,
			appID:          "app_gitea",
			appName:        "Gitea",
			appSlug:        "gitea",
			requestTitle:   "Gitea 连接新增申请",
			titleKeyPrefix: "gitea_connection_connect",
		},
		{
			name:           "gitea_disconnect",
			requestKind:    app.AppInstallationRequestKindDisconnect,
			appID:          "app_gitea",
			appName:        "Gitea",
			appSlug:        "gitea",
			requestTitle:   "Gitea 连接断开申请",
			titleKeyPrefix: "gitea_connection_disconnect",
		},
		{
			name:           "github_connect",
			requestKind:    app.AppInstallationRequestKindConnect,
			appID:          "app_github",
			appName:        "GitHub",
			appSlug:        "github",
			requestTitle:   "GitHub 连接新增申请",
			titleKeyPrefix: "github_connection_connect",
		},
		{
			name:           "github_disconnect",
			requestKind:    app.AppInstallationRequestKindDisconnect,
			appID:          "app_github",
			appName:        "GitHub",
			appSlug:        "github",
			requestTitle:   "GitHub 连接断开申请",
			titleKeyPrefix: "github_connection_disconnect",
		},
		{
			name:           "uninstall",
			requestKind:    app.AppInstallationRequestKindUninstall,
			appName:        "Release App",
			appSlug:        "release-app",
			requestTitle:   "App 卸载申请",
			titleKeyPrefix: "app_uninstallation",
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			request := app.AppInstallationRequestView{
				ID:                     "svcinstreq_minimal_" + testCase.name,
				AppID:                  testCase.appID,
				AppName:                testCase.appName,
				AppSlug:                testCase.appSlug,
				RequestKind:            testCase.requestKind,
				RequestedByMemberID:    "mem_applicant_must_not_leak",
				RequestedByDisplayName: "Alice Applicant Must Not Leak",
				RequestReason:          "reason-must-not-leak",
				BaseConfig:             map[string]any{"base-secret": "base-config-must-not-leak"},
				RequestedConfig:        map[string]any{"requested-secret": "requested-config-must-not-leak"},
				RequestedScopes:        []string{"private.scope.must.not.leak"},
				ConnectionID:           "connection-id-must-not-leak",
				ConnectionTarget:       "connection-target-must-not-leak",
				ConnectionLabel:        "Connection Label Must Not Leak",
				AppRuntimeTransports:   []string{"runner_stream", "callback"},
				AppCallbackURL:         "https://callback-must-not-leak.example.test",
				InstallationID:         "installation-id-must-not-leak",
				Status:                 app.AppInstallationRequestStatusPending,
			}

			expectedRequest := map[string]any{
				"id":        request.ID,
				"appName":   testCase.appName,
				"operation": testCase.requestKind,
			}
			expectedFields := []map[string]any{{
				"label": "operation", "value": testCase.requestKind,
			}}
			if testCase.requestKind == app.AppInstallationRequestKindInstall {
				delete(expectedRequest, "operation")
				expectedFields = []map[string]any{{
					"label": "integration_type", "value": "app",
				}}
			}
			forbidden := []string{
				"reason-must-not-leak",
				"base-config-must-not-leak",
				"requested-config-must-not-leak",
				"private.scope.must.not.leak",
				"connection-id-must-not-leak",
				"connection-target-must-not-leak",
				"Connection Label Must Not Leak",
				"callback-must-not-leak",
				"runner-credential-name-must-not-leak",
				"runner-credential-hint-must-not-leak",
				"installation-id-must-not-leak",
				"mem_applicant_must_not_leak",
				"Alice Applicant Must Not Leak",
				"decision-reason-must-not-leak",
			}
			assertPrivateFieldsAbsent := func(content app.AppNotificationContent) {
				t.Helper()
				for _, value := range forbidden {
					require.NotContains(t, string(content.CardJSON), value)
					require.NotContains(t, content.FallbackText, value)
				}
			}

			content, err := pendingAppInstallationApplicantCardContent(request)
			require.NoError(t, err)
			document := decodeApprovalCard(t, content)
			require.Equal(t, testCase.requestTitle+"已提交", document.Title)
			require.Equal(t, testCase.titleKeyPrefix+"_submitted", document.TitleKey)
			require.Equal(t, app.AppInstallationRequestStatusPending, document.Status)
			require.Equal(t, expectedRequest, document.Request)
			require.Equal(t, expectedFields, document.Fields)
			require.Empty(t, document.Actions)
			require.Contains(t, content.FallbackText, testCase.appName)
			assertPrivateFieldsAbsent(content)

			request.Status = app.AppInstallationRequestStatusApproved
			request.DecisionReason = "decision-reason-must-not-leak"
			resolved, err := resolvedAppInstallationApplicantReceiptCardContent(request, "Workspace Owner")
			require.NoError(t, err)
			resolvedDocument := decodeApprovalCard(t, resolved)
			require.Equal(t, testCase.requestTitle+"已通过", resolvedDocument.Title)
			require.Equal(t, testCase.titleKeyPrefix+"_approved", resolvedDocument.TitleKey)
			require.Equal(t, app.AppInstallationRequestStatusApproved, resolvedDocument.Status)
			require.Equal(t, expectedRequest, resolvedDocument.Request)
			require.Equal(t, expectedFields, resolvedDocument.Fields)
			require.Equal(t, map[string]any{
				"state":         app.AppInstallationRequestStatusApproved,
				"processorName": "Workspace Owner",
			}, resolvedDocument.Approval)
			require.Empty(t, resolvedDocument.Actions)
			assertPrivateFieldsAbsent(resolved)
		})
	}
}

func TestAppConfigurationApprovalCardsShowBaseAndRequestedManifestConfig(t *testing.T) {
	request := app.AppInstallationRequestView{
		ID:                     "svcinstreq_configure",
		AppName:                "Release App",
		AppSlug:                "release-app",
		RequestKind:            app.AppInstallationRequestKindConfigure,
		BaseConfig:             map[string]any{"channel": "general", "enabled": true},
		RequestedConfig:        map[string]any{"channel": "releases", "enabled": false},
		RequestedScopes:        []string{"threads.read", "messages.write"},
		RequestedByDisplayName: "Alice Chen",
		RequestReason:          "Move release announcements",
		Status:                 app.AppInstallationRequestStatusPending,
	}

	pending, err := pendingAppInstallationCardContent(request)
	require.NoError(t, err)
	pendingDocument := decodeApprovalCard(t, pending)
	require.Equal(t, "审批 App 配置变更申请", pendingDocument.Title)
	require.Equal(t, "app_configuration_review", pendingDocument.TitleKey)
	require.Equal(t, app.AppInstallationRequestKindConfigure, pendingDocument.Request["operation"])
	require.Equal(t, request.BaseConfig, pendingDocument.Request["baseConfig"])
	require.Equal(t, request.RequestedConfig, pendingDocument.Request["requestedConfig"])
	require.NotContains(t, pendingDocument.Request, "requestedScopes")
	require.Equal(t, []map[string]any{
		{"label": "operation", "value": app.AppInstallationRequestKindConfigure},
		{"label": "base_config", "value": "{\n  \"channel\": \"general\",\n  \"enabled\": true\n}"},
		{"label": "requested_config", "value": "{\n  \"channel\": \"releases\",\n  \"enabled\": false\n}"},
	}, pendingDocument.Fields)
	require.Contains(t, pending.FallbackText, "变更 App 配置")

	request.Status = app.AppInstallationRequestStatusApproved
	approved, err := resolvedAppInstallationCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	approvedDocument := decodeApprovalCard(t, approved)
	require.Equal(t, "app_configuration_approved", approvedDocument.TitleKey)
	require.NotContains(t, approvedDocument.Request, "requestedScopes")

	result, err := appInstallationApplicantResultCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	resultDocument := decodeApprovalCard(t, result)
	require.Equal(t, "app_configuration_result_approved", resultDocument.TitleKey)
	require.NotContains(t, resultDocument.Request, "requestedScopes")
	require.Len(t, resultDocument.Actions, 1)
	require.Equal(t, "open_app", resultDocument.Actions[0]["actionKey"])
	require.Equal(t, "/apps/release-app", resultDocument.Actions[0]["url"])

	request.Status = app.AppInstallationRequestStatusRejected
	rejected, err := resolvedAppInstallationCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	require.Equal(t, "app_configuration_rejected", decodeApprovalCard(t, rejected).TitleKey)
	rejectedResult, err := appInstallationApplicantResultCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	require.Equal(t, "app_configuration_result_rejected", decodeApprovalCard(t, rejectedResult).TitleKey)
}

func TestAppUninstallApprovalCardsUseExplicitOperationWithoutInternalSnapshot(t *testing.T) {
	request := app.AppInstallationRequestView{
		ID:                     "svcinstreq_uninstall",
		AppName:                "Gitea",
		AppSlug:                "gitea",
		RequestKind:            app.AppInstallationRequestKindUninstall,
		RequestedVersion:       "1.2.3",
		InstallationID:         "appinst_gitea",
		RequestedByDisplayName: "Alice Chen",
		RequestReason:          "No longer needed",
		BaseConfig: map[string]any{
			"installation_updated_at": "must-not-appear",
			"active_connections":      []any{map[string]any{"id": "appcxn_internal"}},
		},
		RequestedScopes: []string{"profile.read"},
		Status:          app.AppInstallationRequestStatusPending,
	}

	pending, err := pendingAppInstallationCardContent(request)
	require.NoError(t, err)
	pendingDocument := decodeApprovalCard(t, pending)
	require.Equal(t, "审批 App 卸载申请", pendingDocument.Title)
	require.Equal(t, "app_uninstallation_review", pendingDocument.TitleKey)
	require.Equal(t, map[string]any{
		"appName":       "Gitea",
		"operation":     app.AppInstallationRequestKindUninstall,
		"reason":        "No longer needed",
		"applicantName": "Alice Chen",
	}, pendingDocument.Request)
	require.NotContains(t, pendingDocument.Request, "id")
	require.NotContains(t, pendingDocument.Request, "installationId")
	require.NotContains(t, pendingDocument.Request, "installedVersion")
	require.NotContains(t, pendingDocument.Request, "baseConfig")
	require.NotContains(t, pendingDocument.Request, "requestedConfig")
	require.NotContains(t, pendingDocument.Request, "requestedScopes")
	require.Equal(t, []map[string]any{{"label": "operation", "value": app.AppInstallationRequestKindUninstall}}, pendingDocument.Fields)
	require.NotContains(t, string(pending.CardJSON), "must-not-appear")
	require.NotContains(t, string(pending.CardJSON), "appcxn_internal")
	require.Contains(t, pending.FallbackText, "卸载 App")

	request.Status = app.AppInstallationRequestStatusApproved
	resolved, err := resolvedAppInstallationCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	resolvedDocument := decodeApprovalCard(t, resolved)
	require.Equal(t, "app_uninstallation_approved", resolvedDocument.TitleKey)
	require.Equal(t, pendingDocument.Request, resolvedDocument.Request)

	result, err := appInstallationApplicantResultCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	resultDocument := decodeApprovalCard(t, result)
	require.Equal(t, "你的 App 卸载申请已通过", resultDocument.Title)
	require.Equal(t, "app_uninstallation_result_approved", resultDocument.TitleKey)
	require.Equal(t, map[string]any{
		"appName":   "Gitea",
		"operation": app.AppInstallationRequestKindUninstall,
		"reason":    "No longer needed",
	}, resultDocument.Request)
	require.Empty(t, resultDocument.Actions, "an approved uninstall must not link to a removed App installation")
}

func TestAppInstallationApprovalCardShowsRuntimeSetupWithoutSecrets(t *testing.T) {
	request := app.AppInstallationRequestView{
		ID:                     "svcinstreq_bot_runtime",
		AppName:                "Release App",
		AppSlug:                "release-app",
		RequestKind:            app.AppInstallationRequestKindInstall,
		RequestedByDisplayName: "Alice Chen",
		Status:                 app.AppInstallationRequestStatusPending,
		AppRuntimeTransports:   []string{"runner_stream", "callback"},
		AppCallbackURL:         "https://app.example.com/agentteam/callback",
		AppCatalogVisibility:   registry.CatalogVisibilityPublic,
		AppDescription:         "Coordinates release notifications",
		CurrentProcessorNames:  []string{"Workspace Owner"},
	}

	content, err := pendingAppInstallationCardContent(request)
	require.NoError(t, err)
	document := decodeApprovalCard(t, content)
	require.Equal(t, "审批应用集成创建申请", document.Title)
	require.Equal(t, "apps_integration_creation_review", document.TitleKey)
	require.Equal(t, "Coordinates release notifications", document.Request["description"])
	require.Equal(t, "app", fieldValue(document.Fields, "integration_type"))
	require.Equal(t, []any{"Workspace Owner"}, document.Approval["currentProcessorNames"])
	require.Equal(t, "runner_stream, callback", fieldValue(document.Fields, "app_runtime_transport"))
	require.Equal(t, request.AppCallbackURL, fieldValue(document.Fields, "app_callback_url"))
	require.Equal(t, registry.CatalogVisibilityPublic, fieldValue(document.Fields, "app_catalog_visibility"))
	require.Equal(t, registry.CatalogVisibilityPublic, document.Request["catalogVisibility"])
	require.Equal(t, map[string]any{
		"transports":  []any{"runner_stream", "callback"},
		"callbackUrl": request.AppCallbackURL,
	}, document.Request["appRuntime"])
	require.NotContains(t, document.Request, "operation")
	require.NotContains(t, document.Request, "baseConfig")
	require.NotContains(t, document.Request, "requestedConfig")
	require.Nil(t, fieldValue(document.Fields, "operation"))
	require.Nil(t, fieldValue(document.Fields, "base_config"))
	require.Nil(t, fieldValue(document.Fields, "requested_config"))
	require.NotContains(t, string(content.CardJSON), "token_hash")
	require.NotContains(t, string(content.CardJSON), "runnerToken")
	require.NotContains(t, string(content.CardJSON), "callbackSigningSecret")

	request.Status = app.AppInstallationRequestStatusApproved
	result, err := appInstallationApplicantResultCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	resultDocument := decodeApprovalCard(t, result)
	require.Equal(t, "你的应用集成创建申请已通过", resultDocument.Title)
	require.Equal(t, "apps_integration_creation_result_approved", resultDocument.TitleKey)
	require.Len(t, resultDocument.Actions, 1)
	require.Equal(t, "open_app_setup", resultDocument.Actions[0]["actionKey"])
	require.Equal(t, "open_url", resultDocument.Actions[0]["type"])
	require.Equal(t, "配置应用", resultDocument.Actions[0]["label"])
	require.Equal(t, "navigate", resultDocument.Actions[0]["semantics"])
	require.Equal(t, "/apps/release-app", resultDocument.Actions[0]["url"])
	require.NotContains(t, string(result.CardJSON), "runnerToken")
	require.NotContains(t, string(result.CardJSON), "callbackSigningSecret")
}

func fieldValue(fields []map[string]any, label string) any {
	for _, field := range fields {
		if field["label"] == label {
			return field["value"]
		}
	}
	return nil
}

func TestAppInstallationDetailURLTargetsAppDetailOrIsOmitted(t *testing.T) {
	require.Equal(t, "/apps/custom%2Fapp", appInstallationDetailURL(app.AppInstallationRequestView{AppSlug: " custom/app "}))
	require.Empty(t, appInstallationDetailURL(app.AppInstallationRequestView{}))
}

func TestGiteaConnectionApprovalCardsShowTargetAndLabelWithoutConfigurationSecrets(t *testing.T) {
	const (
		apiToken     = "connection-api-token-must-not-leak"
		webhookToken = "connection-webhook-secret-must-not-leak"
	)
	testCases := []struct {
		name           string
		requestKind    string
		requestTitle   string
		operationTitle string
		titlePrefix    string
	}{
		{
			name:           "connect",
			requestKind:    app.AppInstallationRequestKindConnect,
			requestTitle:   "Gitea 连接新增申请",
			operationTitle: "新增 Gitea 连接",
			titlePrefix:    "gitea_connection_connect",
		},
		{
			name:           "disconnect",
			requestKind:    app.AppInstallationRequestKindDisconnect,
			requestTitle:   "Gitea 连接断开申请",
			operationTitle: "断开 Gitea 连接",
			titlePrefix:    "gitea_connection_disconnect",
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			request := app.AppInstallationRequestView{
				ID:                     "svcinstreq_gitea_connection_" + testCase.name,
				AppID:                  "app_gitea",
				AppName:                "Gitea",
				AppSlug:                "gitea",
				RequestKind:            testCase.requestKind,
				ConnectionTarget:       "acme/agentteam",
				ConnectionID:           "appcxn_internal_must_not_leak",
				ConnectionLabel:        "Acme/AgentTeam",
				RequestedByDisplayName: "Alice Chen",
				RequestReason:          testCase.operationTitle,
				BaseConfig: map[string]any{
					"api_token":      apiToken,
					"webhook_secret": webhookToken,
				},
				RequestedConfig: map[string]any{
					"target":         "private-target-must-not-leak",
					"api_token":      apiToken,
					"webhook_secret": webhookToken,
				},
				RequestedScopes: []string{"private.scope.must.not.leak"},
				Status:          app.AppInstallationRequestStatusPending,
			}

			pending, err := pendingAppInstallationCardContent(request)
			require.NoError(t, err)
			pendingDocument := decodeApprovalCard(t, pending)
			require.Equal(t, "审批 "+testCase.requestTitle, pendingDocument.Title)
			require.Equal(t, testCase.titlePrefix+"_review", pendingDocument.TitleKey)
			require.Equal(t, app.AppInstallationRequestStatusPending, pendingDocument.Status)
			require.Len(t, pendingDocument.Actions, 2)
			require.Equal(t, ActionApprove, pendingDocument.Actions[0]["actionKey"])
			require.Equal(t, ActionReject, pendingDocument.Actions[1]["actionKey"])
			require.Equal(t, testCase.requestKind, pendingDocument.Request["operation"])
			require.Equal(t, "acme/agentteam", pendingDocument.Request["connectionTarget"])
			require.Equal(t, "Acme/AgentTeam", pendingDocument.Request["connectionLabel"])
			require.Equal(t, "Alice Chen", pendingDocument.Request["applicantName"])
			require.Equal(t, []map[string]any{
				{"label": "operation", "value": testCase.requestKind},
				{"label": "connection_target", "value": "acme/agentteam"},
				{"label": "connection_label", "value": "Acme/AgentTeam"},
			}, pendingDocument.Fields)
			require.Contains(t, pending.FallbackText, testCase.operationTitle)
			assertGiteaConnectionCardDoesNotLeakInternalConfig(t, pending, apiToken, webhookToken)

			request.Status = app.AppInstallationRequestStatusApproved
			resolved, err := resolvedAppInstallationCardContent(request, "Workspace Owner")
			require.NoError(t, err)
			resolvedDocument := decodeApprovalCard(t, resolved)
			require.Equal(t, testCase.requestTitle+"已通过", resolvedDocument.Title)
			require.Equal(t, testCase.titlePrefix+"_approved", resolvedDocument.TitleKey)
			require.Equal(t, "acme/agentteam", resolvedDocument.Request["connectionTarget"])
			require.Equal(t, "Acme/AgentTeam", resolvedDocument.Request["connectionLabel"])
			assertGiteaConnectionCardDoesNotLeakInternalConfig(t, resolved, apiToken, webhookToken)

			applicantResult, err := appInstallationApplicantResultCardContent(request, "Workspace Owner")
			require.NoError(t, err)
			applicantResultDocument := decodeApprovalCard(t, applicantResult)
			require.Equal(t, "你的 "+testCase.requestTitle+"已通过", applicantResultDocument.Title)
			require.Equal(t, testCase.titlePrefix+"_result_approved", applicantResultDocument.TitleKey)
			require.Len(t, applicantResultDocument.Actions, 1)
			require.Equal(t, "/apps/gitea", applicantResultDocument.Actions[0]["url"])
			require.NotContains(t, applicantResultDocument.Request, "applicantName")
			assertGiteaConnectionCardDoesNotLeakInternalConfig(t, applicantResult, apiToken, webhookToken)

			request.Status = app.AppInstallationRequestStatusRejected
			rejected, err := resolvedAppInstallationCardContent(request, "Workspace Owner")
			require.NoError(t, err)
			require.Equal(t, testCase.titlePrefix+"_rejected", decodeApprovalCard(t, rejected).TitleKey)
			rejectedResult, err := appInstallationApplicantResultCardContent(request, "Workspace Owner")
			require.NoError(t, err)
			require.Equal(t, testCase.titlePrefix+"_result_rejected", decodeApprovalCard(t, rejectedResult).TitleKey)
			require.Empty(t, decodeApprovalCard(t, rejectedResult).Actions)
		})
	}
}

func assertGiteaConnectionCardDoesNotLeakInternalConfig(
	t testing.TB,
	content app.AppNotificationContent,
	forbidden ...string,
) {
	t.Helper()

	encoded := string(content.CardJSON)
	for _, value := range append(forbidden,
		"appcxn_internal_must_not_leak",
		"private-target-must-not-leak",
		"private.scope.must.not.leak",
	) {
		require.NotContains(t, encoded, value)
	}
	document := decodeApprovalCard(t, content)
	for _, key := range []string{"connectionId", "baseConfig", "requestedConfig", "requestedScopes"} {
		require.NotContains(t, document.Request, key)
	}
}

func TestProviderConnectionApprovalCardsRequireExplicitSafeProjection(t *testing.T) {
	testCases := []struct {
		name      string
		request   app.AppInstallationRequestView
		wantError string
	}{
		{
			name: "wrong App",
			request: app.AppInstallationRequestView{
				AppID:            "app_slack",
				AppSlug:          "slack",
				RequestKind:      app.AppInstallationRequestKindConnect,
				ConnectionTarget: "acme/agentteam",
				ConnectionLabel:  "Acme/AgentTeam",
			},
			wantError: "provider connection request must target the GitHub or Gitea App",
		},
		{
			name: "missing target does not fall back to internal config",
			request: app.AppInstallationRequestView{
				AppID:           "app_gitea",
				AppSlug:         "gitea",
				RequestKind:     app.AppInstallationRequestKindConnect,
				ConnectionLabel: "Acme/AgentTeam",
				RequestedConfig: map[string]any{"target": "acme/agentteam"},
			},
			wantError: "provider connection request target is required",
		},
		{
			name: "missing label",
			request: app.AppInstallationRequestView{
				AppID:            "app_gitea",
				AppSlug:          "gitea",
				RequestKind:      app.AppInstallationRequestKindDisconnect,
				ConnectionTarget: "acme/agentteam",
			},
			wantError: "provider connection request label is required",
		},
	}
	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			testCase.request.Status = app.AppInstallationRequestStatusPending
			_, err := pendingAppInstallationCardContent(testCase.request)
			require.EqualError(t, err, testCase.wantError)
		})
	}
}

func TestGitHubConnectionApprovalCardsUseSharedSafeProjection(t *testing.T) {
	request := app.AppInstallationRequestView{
		ID:                     "svcinstreq_github_connection",
		AppID:                  "app_github",
		AppName:                "GitHub",
		AppSlug:                "github",
		RequestKind:            app.AppInstallationRequestKindConnect,
		ConnectionTarget:       "789",
		ConnectionLabel:        "acme",
		RequestedByDisplayName: "Alice Chen",
		RequestedConfig: map[string]any{
			"github_config_id":         "ghcfg_internal_must_not_leak",
			"github_config_updated_at": "2026-07-21T00:00:00Z",
			"webhook_secret":           "github-webhook-secret-must-not-leak",
		},
		Status: app.AppInstallationRequestStatusPending,
	}
	pending, err := pendingAppInstallationCardContent(request)
	require.NoError(t, err)
	document := decodeApprovalCard(t, pending)
	require.Equal(t, "审批 GitHub 连接新增申请", document.Title)
	require.Equal(t, "github_connection_connect_review", document.TitleKey)
	require.Equal(t, "789", document.Request["connectionTarget"])
	require.Equal(t, "acme", document.Request["connectionLabel"])
	require.Equal(t, []map[string]any{
		{"label": "operation", "value": "connect"},
		{"label": "connection_target", "value": "789"},
		{"label": "connection_label", "value": "acme"},
	}, document.Fields)
	require.NotContains(t, string(pending.CardJSON), "ghcfg_internal_must_not_leak")
	require.NotContains(t, string(pending.CardJSON), "github-webhook-secret-must-not-leak")
}

func TestGitHubInitialSetupApprovalCardsListEverySafeTargetAndKeepPatchingApproveOnly(t *testing.T) {
	request := app.AppInstallationRequestView{
		ID:                     "svcinstreq_github_initial",
		AppID:                  "app_github",
		AppName:                "GitHub",
		AppSlug:                "github",
		RequestKind:            app.AppInstallationRequestKindInstall,
		RequestedByDisplayName: "Alice Chen",
		GitHubConnectionTargets: []app.GitHubConnectionTargetView{
			{InstallationID: "41", AccountLogin: "pingcap"},
			{InstallationID: "702", AccountLogin: "acme-labs"},
		},
		Status:           app.AppInstallationRequestStatusPending,
		ApprovalRequired: true,
	}

	pending, err := pendingAppInstallationCardContent(request)
	require.NoError(t, err)
	pendingDocument := decodeApprovalCard(t, pending)
	require.Equal(t, []any{
		map[string]any{"installationId": "41", "accountLogin": "pingcap"},
		map[string]any{"installationId": "702", "accountLogin": "acme-labs"},
	}, pendingDocument.Request["connectionTargets"])
	require.Equal(t, []map[string]any{
		{"label": "operation", "value": app.AppInstallationRequestKindInstall},
		{"label": "github_connection_target", "value": "pingcap (installation 41)"},
		{"label": "github_connection_target", "value": "acme-labs (installation 702)"},
	}, pendingDocument.Fields)
	require.Len(t, pendingDocument.Actions, 2)
	require.Equal(t, int64(1), appInstallationRequestBusinessVersion(request))

	request.Status = app.AppInstallationRequestStatusSetupPatching
	patching, err := pendingAppInstallationCardContent(request)
	require.NoError(t, err)
	patchingDocument := decodeApprovalCard(t, patching)
	require.Equal(t, app.AppInstallationRequestStatusSetupPatching, patchingDocument.Status)
	require.Equal(t, appInstallationSetupPatchingRetryTextKey, patchingDocument.TextKey)
	require.Equal(t, appInstallationSetupPatchingRetryText, patchingDocument.Text)
	require.Equal(t, []map[string]any{{
		"actionKey": ActionApprove,
		"type":      "callback",
		"label":     "重试通过",
		"semantics": "commit",
	}}, patchingDocument.Actions)
	require.Equal(t, pendingDocument.Request["connectionTargets"], patchingDocument.Request["connectionTargets"])
	require.Equal(t, int64(1), appInstallationRequestBusinessVersion(request))

	request.Status = app.AppInstallationRequestStatusApproved
	request.DecidedByMemberID = "mem_owner"
	resolved, err := resolvedAppInstallationCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	resolvedDocument := decodeApprovalCard(t, resolved)
	require.Equal(t, pendingDocument.Request["connectionTargets"], resolvedDocument.Request["connectionTargets"])
	require.Empty(t, resolvedDocument.Actions)
	require.Equal(t, int64(2), appInstallationRequestBusinessVersion(request))
	require.NotContains(t, string(resolved.CardJSON), "accountId")
}

func TestGiteaAppInstallationApprovalCardsRedactCredentials(t *testing.T) {
	const (
		apiTokenEnvelope = `{"_storage":"aes-gcm-v1","ciphertext":"api-token-ciphertext"}`
		webhookSecret    = "raw-webhook-secret-must-not-leak"
		baseAPIToken     = "base-api-token-must-not-leak"
		baseWebhook      = `{"_storage":"aes-gcm-v1","ciphertext":"base-webhook-ciphertext"}`
	)

	testCases := []struct {
		name                   string
		requestKind            string
		requestedWebhookSecret any
	}{
		{
			name:                   "install",
			requestKind:            app.AppInstallationRequestKindInstall,
			requestedWebhookSecret: webhookSecret,
		},
		{
			name:                   "configure replaces token and keeps webhook secret",
			requestKind:            app.AppInstallationRequestKindConfigure,
			requestedWebhookSecret: "",
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			request := app.AppInstallationRequestView{
				ID:                     "svcinstreq_gitea_" + testCase.requestKind,
				AppName:                "Gitea",
				AppSlug:                "gitea",
				RequestKind:            testCase.requestKind,
				RequestedByDisplayName: "Alice Chen",
				BaseConfig: map[string]any{
					"instance_url":   "https://old-gitea.example.com",
					"display_name":   "Old Gitea",
					"api_token":      baseAPIToken,
					"webhook_secret": baseWebhook,
				},
				RequestedConfig: map[string]any{
					"instance_url":   "https://gitea.example.com",
					"display_name":   "Team Gitea",
					"api_token":      apiTokenEnvelope,
					"webhook_secret": testCase.requestedWebhookSecret,
				},
				RequestReason:   "Connect the team Gitea instance",
				RequestedScopes: []string{"profile.read", "messages.write"},
				Status:          app.AppInstallationRequestStatusPending,
			}

			pending, err := pendingAppInstallationCardContent(request)
			require.NoError(t, err)
			assertGiteaApprovalCardRedacted(
				t, pending, testCase.requestKind, true,
			)

			request.Status = app.AppInstallationRequestStatusApproved
			request.InstallationID = "appinst_gitea"
			resolved, err := resolvedAppInstallationCardContent(request, "Workspace Owner")
			require.NoError(t, err)
			assertGiteaApprovalCardRedacted(
				t, resolved, testCase.requestKind, true,
			)

			result, err := appInstallationApplicantResultCardContent(request, "Workspace Owner")
			require.NoError(t, err)
			assertGiteaApprovalCardRedacted(
				t, result, testCase.requestKind, false,
			)
		})
	}
}

func assertGiteaApprovalCardRedacted(
	t testing.TB,
	content app.AppNotificationContent,
	requestKind string,
	wantApplicant bool,
) {
	t.Helper()

	cardJSON := string(content.CardJSON)
	for _, forbidden := range []string{
		"api-token-ciphertext",
		"raw-webhook-secret-must-not-leak",
		"base-api-token-must-not-leak",
		"base-webhook-ciphertext",
		"aes-gcm-v1",
		"ciphertext",
	} {
		require.NotContains(t, cardJSON, forbidden)
	}

	document := decodeApprovalCard(t, content)
	require.Equal(t, requestKind, document.Request["operation"])
	require.NotContains(t, document.Request, "requestedScopes")
	if wantApplicant {
		require.Equal(t, "Alice Chen", document.Request["applicantName"])
	} else {
		require.NotContains(t, document.Request, "applicantName")
	}
	require.Equal(t, map[string]any{
		"instance_url": "https://old-gitea.example.com",
		"display_name": "Old Gitea",
	}, document.Request["baseConfig"])
	require.Equal(t, map[string]any{
		"instance_url":   "https://gitea.example.com",
		"display_name":   "Team Gitea",
		"api_token":      "********",
		"webhook_secret": "********",
	}, document.Request["requestedConfig"])
	require.Equal(t, []map[string]any{
		{"label": "operation", "value": requestKind},
		{"label": "instance_url", "value": "https://gitea.example.com"},
		{"label": "display_name", "value": "Team Gitea"},
		{"label": "api_token", "value": "********"},
		{"label": "webhook_secret", "value": "********"},
	}, document.Fields)
}

func TestGitHubAppInstallationApprovalCardsUseSafeProjection(t *testing.T) {
	request := app.AppInstallationRequestView{
		ID:                     "svcinstreq_github",
		AppID:                  "app_github",
		AppName:                "GitHub",
		AppSlug:                "github",
		RequestKind:            app.AppInstallationRequestKindInstall,
		RequestedByDisplayName: "Alice Chen",
		BaseConfig: map[string]any{
			"app_name":       "Old GitHub App",
			"owner_login":    "old-owner",
			"client_secret":  "base-client-secret-must-not-leak",
			"unknown_secret": "base-unknown-secret-must-not-leak",
		},
		RequestedConfig: map[string]any{
			"app_id":          "4242",
			"app_slug":        "agentteam-github",
			"app_name":        "AgentTeam GitHub",
			"html_url":        "https://github.com/apps/agentteam-github",
			"client_id":       "Iv1.client-id",
			"client_secret":   `{"_storage":"aes-gcm-v1","ciphertext":"client-secret-ciphertext"}`,
			"private_key_pem": "private-key-must-not-leak",
			"webhook_secret": map[string]any{
				"_storage":   "aes-gcm-v1",
				"ciphertext": "webhook-secret-ciphertext",
			},
			"owner_login":           "pingcap",
			"owner_id":              "1234",
			"unknown_secret":        "unknown-secret-must-not-leak",
			"future_internal_state": map[string]any{"ciphertext": "future-secret-ciphertext"},
		},
		RequestReason: "Connect GitHub automation",
		Status:        app.AppInstallationRequestStatusPending,
	}

	pending, err := pendingAppInstallationCardContent(request)
	require.NoError(t, err)
	assertGitHubApprovalCardSafe(t, pending, true)

	request.Status = app.AppInstallationRequestStatusApproved
	request.InstallationID = "appinst_github"
	resolved, err := resolvedAppInstallationCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	assertGitHubApprovalCardSafe(t, resolved, true)

	result, err := appInstallationApplicantResultCardContent(request, "Workspace Owner")
	require.NoError(t, err)
	assertGitHubApprovalCardSafe(t, result, false)
}

func assertGitHubApprovalCardSafe(
	t testing.TB,
	content app.AppNotificationContent,
	wantApplicant bool,
) {
	t.Helper()

	cardJSON := string(content.CardJSON)
	for _, forbidden := range []string{
		"client-secret-ciphertext",
		"private-key-must-not-leak",
		"webhook-secret-ciphertext",
		"unknown-secret-must-not-leak",
		"base-client-secret-must-not-leak",
		"base-unknown-secret-must-not-leak",
		"future-secret-ciphertext",
		"future_internal_state",
		"unknown_secret",
		"aes-gcm-v1",
		"ciphertext",
	} {
		require.NotContains(t, cardJSON, forbidden)
	}

	document := decodeApprovalCard(t, content)
	require.Equal(t, app.AppInstallationRequestKindInstall, document.Request["operation"])
	if wantApplicant {
		require.Equal(t, "Alice Chen", document.Request["applicantName"])
	} else {
		require.NotContains(t, document.Request, "applicantName")
	}
	require.Equal(t, map[string]any{
		"app_name":      "Old GitHub App",
		"owner_login":   "old-owner",
		"client_secret": "********",
	}, document.Request["baseConfig"])
	require.Equal(t, map[string]any{
		"app_id":          "4242",
		"app_slug":        "agentteam-github",
		"app_name":        "AgentTeam GitHub",
		"html_url":        "https://github.com/apps/agentteam-github",
		"client_id":       "Iv1.client-id",
		"client_secret":   "********",
		"private_key_pem": "********",
		"webhook_secret":  "********",
		"owner_login":     "pingcap",
		"owner_id":        "1234",
	}, document.Request["requestedConfig"])
	require.Equal(t, []map[string]any{
		{"label": "operation", "value": app.AppInstallationRequestKindInstall},
		{"label": "app_name", "value": "AgentTeam GitHub"},
		{"label": "owner_login", "value": "pingcap"},
		{"label": "app_slug", "value": "agentteam-github"},
		{"label": "html_url", "value": "https://github.com/apps/agentteam-github"},
		{"label": "app_id", "value": "4242"},
		{"label": "owner_id", "value": "1234"},
		{"label": "client_id", "value": "Iv1.client-id"},
		{"label": "client_secret", "value": "********"},
		{"label": "private_key_pem", "value": "********"},
		{"label": "webhook_secret", "value": "********"},
	}, document.Fields)
}

func TestAppInstallationApprovalCardsRejectInvalidOperation(t *testing.T) {
	for _, requestKind := range []string{"", "rotate_credentials"} {
		t.Run(requestKind, func(t *testing.T) {
			request := app.AppInstallationRequestView{
				ID:              "svcinstreq_invalid",
				AppName:         "Release App",
				RequestKind:     requestKind,
				RequestedConfig: map[string]any{},
				Status:          app.AppInstallationRequestStatusPending,
			}

			_, err := pendingAppInstallationCardContent(request)
			require.EqualError(t, err, fmt.Sprintf("unsupported App installation request kind %q", requestKind))
		})
	}
}
