package serviceaccount_test

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

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

	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	. "github.com/tidbcloud/AgentTeam/internal/api/testkit"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	"github.com/tidbcloud/AgentTeam/internal/store/attachment"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/serviceaccount"
)

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

	runtime := StartRuntime(t)
	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()
	client := server.Client()

	owner := RegisterUserForOrganizationTests(t, client, server.URL, "service-account-direct-fence-owner@example.com", "service-account-direct-fence-owner")
	var workspaces []dto.WorkspaceResponse
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(owner.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	admin := AcceptTaskHTTPInvite(
		t, runtime, client, server.URL, owner.User.ID, workspaceID,
		"service-account-direct-fence-admin@example.com", "service-account-direct-fence-admin",
	)
	member := AcceptTaskHTTPInvite(
		t, runtime, client, server.URL, owner.User.ID, workspaceID,
		"service-account-direct-fence-member@example.com", "service-account-direct-fence-member",
	)
	adminMemberID := TaskWorkspaceMemberIDForUser(t, runtime, workspaceID, admin.User.ID)
	memberMemberID := TaskWorkspaceMemberIDForUser(t, runtime, workspaceID, member.User.ID)
	response = DoJSON(t, client, http.MethodPatch, server.URL+"/api/v1/workspaces/"+workspaceID+"/members/"+adminMemberID, map[string]any{
		"role": "admin",
	}, AuthHeaders(owner.AccessToken), nil)
	require.Equal(t, http.StatusOK, response.StatusCode)

	type directCreateResponse struct {
		Request struct {
			ID                 string `json:"id"`
			AuthorizationPath  string `json:"authorizationPath"`
			ApprovalInstanceID string `json:"approvalInstanceId"`
		} `json:"request"`
		Account *struct {
			ID string `json:"id"`
		} `json:"account"`
	}
	createPath := fmt.Sprintf("%s/api/v1/workspaces/%s/service-account-create-requests", server.URL, workspaceID)
	omittedIntentClientID := uuid.NewString()
	response = DoJSON(t, client, http.MethodPost, createPath, map[string]any{
		"clientRequestId": omittedIntentClientID,
		"name":            "Ambiguous Authorization Must Fail",
	}, WorkspaceHeaders(admin.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusBadRequest, response.StatusCode)
	var omittedIntentRequestCount int64
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM service_account_create_requests
WHERE workspace_id = $1
  AND requested_by_member_id = $2
  AND client_request_id = $3
`, workspaceID, adminMemberID, omittedIntentClientID).Scan(&omittedIntentRequestCount))
	require.Zero(t, omittedIntentRequestCount, "HTTP callers must declare direct-create or approval intent before any durable write")

	var direct directCreateResponse
	response = DoJSON(t, client, http.MethodPost, createPath, map[string]any{
		"clientRequestId":           uuid.NewString(),
		"expectedAuthorizationPath": serviceaccount.AuthorizationPathDirectCreate,
		"name":                      "Admin Direct News",
	}, WorkspaceHeaders(admin.AccessToken, workspaceID), &direct)
	require.Equal(t, http.StatusCreated, response.StatusCode)
	require.Equal(t, serviceaccount.AuthorizationPathDirectCreate, direct.Request.AuthorizationPath)
	require.Empty(t, direct.Request.ApprovalInstanceID)
	require.NotNil(t, direct.Account)

	var approvalCount, approvalCardCount int64
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM approval_instances
WHERE workspace_id = $1
  AND subject_type = 'service_account_create_request'
`, workspaceID).Scan(&approvalCount))
	require.Zero(t, approvalCount)
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM service_notifications
WHERE workspace_id = $1
  AND business_object_namespace = 'agentteam.approval'
`, workspaceID).Scan(&approvalCardCount))
	require.Zero(t, approvalCardCount)

	staleDirectClientID := uuid.NewString()
	response = DoJSON(t, client, http.MethodPost, createPath, map[string]any{
		"clientRequestId":           staleDirectClientID,
		"expectedAuthorizationPath": serviceaccount.AuthorizationPathDirectCreate,
		"name":                      "Must Not Become Approval",
	}, WorkspaceHeaders(member.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusForbidden, response.StatusCode)
	var staleRequestCount int64
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM service_account_create_requests
WHERE workspace_id = $1
  AND requested_by_member_id = $2
  AND client_request_id = $3
`, workspaceID, memberMemberID, staleDirectClientID).Scan(&staleRequestCount))
	require.Zero(t, staleRequestCount)
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM approval_instances
WHERE workspace_id = $1
  AND subject_type = 'service_account_create_request'
`, workspaceID).Scan(&approvalCount))
	require.Zero(t, approvalCount)

	var pending directCreateResponse
	pendingClientID := uuid.NewString()
	response = DoJSON(t, client, http.MethodPost, createPath, map[string]any{
		"clientRequestId":           pendingClientID,
		"expectedAuthorizationPath": serviceaccount.AuthorizationPathApprovalRequired,
		"name":                      "Member Approval News",
	}, WorkspaceHeaders(member.AccessToken, workspaceID), &pending)
	require.Equal(t, http.StatusAccepted, response.StatusCode)
	require.Equal(t, serviceaccount.AuthorizationPathApprovalRequired, pending.Request.AuthorizationPath)
	require.NotEmpty(t, pending.Request.ApprovalInstanceID)
	require.Nil(t, pending.Account)
	response = DoJSON(t, client, http.MethodPatch, server.URL+"/api/v1/workspaces/"+workspaceID+"/members/"+memberMemberID, map[string]any{
		"role": "admin",
	}, AuthHeaders(owner.AccessToken), nil)
	require.Equal(t, http.StatusOK, response.StatusCode)
	response = DoJSON(t, client, http.MethodPost, createPath, map[string]any{
		"clientRequestId":           pendingClientID,
		"expectedAuthorizationPath": serviceaccount.AuthorizationPathDirectCreate,
		"name":                      "Member Approval News",
	}, WorkspaceHeaders(member.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusConflict, response.StatusCode, "promoted request replay must commit cleanup but never return a cancelled success payload")
	var promotedState, cancelReceipt string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT business_state, cancel_approval_receipt_ref
FROM service_account_create_requests
WHERE workspace_id = $1 AND requested_by_member_id = $2 AND client_request_id = $3
`, workspaceID, memberMemberID, pendingClientID).Scan(&promotedState, &cancelReceipt))
	require.Equal(t, serviceaccount.RequestStateCancelled, promotedState)
	require.Equal(t, "Withdrawn by system: workspace administrators use direct Service Account management.", cancelReceipt)
}

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

	runtime := StartRuntime(t)
	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()
	client := server.Client()

	owner := RegisterUserForOrganizationTests(t, client, server.URL, "service-account-delete-owner@example.com", "service-account-delete-owner")
	var workspaces []dto.WorkspaceResponse
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(owner.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID

	manager := AcceptTaskHTTPInvite(
		t, runtime, client, server.URL, owner.User.ID, workspaceID,
		"service-account-delete-manager@example.com", "service-account-delete-manager",
	)
	peer := AcceptTaskHTTPInvite(
		t, runtime, client, server.URL, owner.User.ID, workspaceID,
		"service-account-delete-peer@example.com", "service-account-delete-peer",
	)
	managerMemberID := TaskWorkspaceMemberIDForUser(t, runtime, workspaceID, manager.User.ID)

	type accountView struct {
		ID        string `json:"id"`
		Version   int64  `json:"version"`
		CanManage bool   `json:"canManage"`
	}
	type createResponse struct {
		Account *accountView `json:"account"`
	}
	createPath := fmt.Sprintf("%s/api/v1/workspaces/%s/service-account-create-requests", server.URL, workspaceID)
	var created createResponse
	response = DoJSON(t, client, http.MethodPost, createPath, map[string]any{
		"clientRequestId":           uuid.NewString(),
		"expectedAuthorizationPath": serviceaccount.AuthorizationPathDirectCreate,
		"name":                      "Direct Manager Deletion",
	}, WorkspaceHeaders(owner.AccessToken, workspaceID), &created)
	require.Equal(t, http.StatusCreated, response.StatusCode)
	require.NotNil(t, created.Account)

	accountURL := fmt.Sprintf("%s/api/v1/workspaces/%s/service-accounts/%s", server.URL, workspaceID, created.Account.ID)
	response = DoJSON(t, client, http.MethodPost, accountURL+"/managers", map[string]any{
		"memberId": managerMemberID,
	}, WorkspaceHeaders(owner.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusOK, response.StatusCode)

	var managerView accountView
	response = DoJSON(t, client, http.MethodGet, accountURL, nil, WorkspaceHeaders(manager.AccessToken, workspaceID), &managerView)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.True(t, managerView.CanManage)
	require.Positive(t, managerView.Version)

	var peerView map[string]any
	response = DoJSON(t, client, http.MethodGet, accountURL, nil, WorkspaceHeaders(peer.AccessToken, workspaceID), &peerView)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, false, peerView["canManage"])
	require.NotContains(t, peerView, "version")
	response = DoJSON(t, client, http.MethodDelete, accountURL+fmt.Sprintf(
		"?expectedVersion=%d", managerView.Version+97,
	), nil, WorkspaceHeaders(peer.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusForbidden, response.StatusCode, "an unrelated member must not use a stale version as an account-version oracle")

	response = DoJSON(t, client, http.MethodDelete, accountURL+fmt.Sprintf(
		"?expectedVersion=%d", managerView.Version+1,
	), nil, WorkspaceHeaders(manager.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusConflict, response.StatusCode, "direct manager deletion must preserve the version fence")

	var deleted accountView
	response = DoJSON(t, client, http.MethodDelete, accountURL+fmt.Sprintf(
		"?expectedVersion=%d", managerView.Version,
	), nil, WorkspaceHeaders(manager.AccessToken, workspaceID), &deleted)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, created.Account.ID, deleted.ID)
	require.True(t, deleted.CanManage)
	response = DoJSON(t, client, http.MethodGet, accountURL, nil, WorkspaceHeaders(owner.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusNotFound, response.StatusCode)
}

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

	runtime := StartRuntime(t)
	router := gin.New()
	runtime.RegisterRoutes(router)
	server := httptest.NewServer(router)
	defer server.Close()
	client := server.Client()

	owner := RegisterUserForOrganizationTests(t, client, server.URL, "service-account-owner@example.com", "service-account-owner")
	var workspaces []dto.WorkspaceResponse
	response := DoJSON(t, client, http.MethodGet, server.URL+"/api/v1/workspaces", nil, AuthHeaders(owner.AccessToken), &workspaces)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, workspaces)
	workspaceID := workspaces[0].ID
	ownerMemberID := workspaces[0].MemberID

	publisher := AcceptTaskHTTPInvite(
		t, runtime, client, server.URL, owner.User.ID, workspaceID,
		"service-account-publisher@example.com", "service-account-publisher",
	)
	publisherMemberID := TaskWorkspaceMemberIDForUser(t, runtime, workspaceID, publisher.User.ID)

	type requestView struct {
		ID                      string   `json:"id"`
		RequestedByMemberID     string   `json:"requestedByMemberId"`
		AuthorizationPath       string   `json:"authorizationPath"`
		BusinessState           string   `json:"businessState"`
		ApprovalInstanceID      string   `json:"approvalInstanceId"`
		CreatedServiceAccountID string   `json:"createdServiceAccountId"`
		CurrentProcessorNames   []string `json:"currentProcessorNames"`
		ProcessorName           string   `json:"processorName"`
	}
	type accountView struct {
		ID                string `json:"id"`
		MessageScopeID    string `json:"messageScopeId"`
		AvatarURL         string `json:"avatarUrl"`
		CanManage         bool   `json:"canManage"`
		CanPublish        bool   `json:"canPublish"`
		CanModerate       bool   `json:"canModerate"`
		CanManagePlatform bool   `json:"canManagePlatform"`
		CreatedByMemberID string `json:"createdByMemberId"`
		Version           int64  `json:"version"`
	}
	type managerView struct {
		ID       string `json:"id"`
		MemberID string `json:"memberId"`
		Status   string `json:"status"`
		Version  int64  `json:"version"`
	}
	type createResponse struct {
		Request requestView  `json:"request"`
		Account *accountView `json:"account"`
	}

	createPath := fmt.Sprintf("%s/api/v1/workspaces/%s/service-account-create-requests", server.URL, workspaceID)
	var direct createResponse
	response = DoJSON(t, client, http.MethodPost, createPath, map[string]any{
		"clientRequestId":           uuid.NewString(),
		"expectedAuthorizationPath": serviceaccount.AuthorizationPathDirectCreate,
		"name":                      "Release News",
	}, WorkspaceHeaders(owner.AccessToken, workspaceID), &direct)
	require.Equal(t, http.StatusCreated, response.StatusCode)
	require.Equal(t, serviceaccount.AuthorizationPathDirectCreate, direct.Request.AuthorizationPath)
	require.Equal(t, serviceaccount.RequestStateFulfilled, direct.Request.BusinessState)
	require.NotNil(t, direct.Account)
	require.True(t, direct.Account.CanManage)
	require.True(t, direct.Account.CanPublish)
	require.True(t, direct.Account.CanModerate)
	require.Equal(t, ownerMemberID, direct.Account.CreatedByMemberID)
	require.Equal(t, direct.Account.ID, direct.Request.CreatedServiceAccountID)
	var serviceInstallationID, authorMemberID string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT service_installation_id, author_member_id
FROM service_accounts
WHERE workspace_id = $1 AND id = $2
`, workspaceID, direct.Account.ID).Scan(&serviceInstallationID, &authorMemberID))

	var readerAccountList struct {
		ServiceAccounts []map[string]any `json:"serviceAccounts"`
		NextCursor      string           `json:"nextCursor"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+fmt.Sprintf("/api/v1/workspaces/%s/service-accounts", workspaceID), nil, WorkspaceHeaders(publisher.AccessToken, workspaceID), &readerAccountList)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, readerAccountList.ServiceAccounts, 1)
	readerAccountJSON, err := json.Marshal(readerAccountList.ServiceAccounts[0])
	require.NoError(t, err)
	var readerAccount accountView
	require.NoError(t, json.Unmarshal(readerAccountJSON, &readerAccount))
	require.Equal(t, direct.Account.ID, readerAccount.ID)
	require.False(t, readerAccount.CanManage)
	require.False(t, readerAccount.CanPublish)
	require.False(t, readerAccount.CanModerate)
	privateAccountFields := []string{
		"workspaceId", "platformServiceAccountId", "serviceInstallationId",
		"serviceMemberId", "authorMemberId", "avatarAssetId",
		"appliedPlatformProfileVersion", "audiencePolicyVersion",
		"effectiveAudienceVersion", "audienceState", "version",
		"createdByMemberId", "createdByOperatorUserId", "createdAt", "updatedAt",
	}
	for _, privateField := range privateAccountFields {
		require.NotContains(t, readerAccountList.ServiceAccounts[0], privateField)
	}
	accountURL := server.URL + fmt.Sprintf("/api/v1/workspaces/%s/service-accounts/%s", workspaceID, direct.Account.ID)
	var readerAccountDetail map[string]any
	response = DoJSON(t, client, http.MethodGet, accountURL, nil, WorkspaceHeaders(publisher.AccessToken, workspaceID), &readerAccountDetail)
	require.Equal(t, http.StatusOK, response.StatusCode)
	for _, privateField := range privateAccountFields {
		require.NotContains(t, readerAccountDetail, privateField)
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+fmt.Sprintf("/api/v1/workspaces/%s/service-accounts?q=release+news&limit=100", workspaceID), nil, WorkspaceHeaders(publisher.AccessToken, workspaceID), &readerAccountList)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, readerAccountList.ServiceAccounts, 1)
	require.Equal(t, direct.Account.ID, readerAccountList.ServiceAccounts[0]["id"])

	var managerList struct {
		Managers []managerView `json:"managers"`
	}
	managerPath := server.URL + fmt.Sprintf("/api/v1/workspaces/%s/service-accounts/%s/managers", workspaceID, direct.Account.ID)
	response = DoJSON(t, client, http.MethodGet, managerPath, nil, WorkspaceHeaders(publisher.AccessToken, workspaceID), &managerList)
	require.Equal(t, http.StatusForbidden, response.StatusCode)
	require.Empty(t, managerList.Managers)
	var addedManager managerView
	response = DoJSON(t, client, http.MethodPost, managerPath, map[string]any{
		"memberId": publisherMemberID,
	}, WorkspaceHeaders(owner.AccessToken, workspaceID), &addedManager)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, publisherMemberID, addedManager.MemberID)
	require.Equal(t, serviceaccount.ManagerStatusActive, addedManager.Status)
	require.Positive(t, addedManager.Version)
	avatarAssetID, err := runtime.Store.Attachment().UploadResourceAvatar(
		runtime.Ctx, workspaceID, "service_account", direct.Account.ID,
		attachment.CreateAttachmentParams{
			Filename: "service-account.png", ContentType: "image/png", Blob: []byte("service-account-avatar"),
		},
	)
	require.NoError(t, err)
	var avatarUpdated accountView
	response = DoJSON(t, client, http.MethodPatch, server.URL+fmt.Sprintf(
		"/api/v1/workspaces/%s/service-accounts/%s/avatar", workspaceID, direct.Account.ID,
	), map[string]any{
		"avatarAssetId": avatarAssetID,
	}, WorkspaceHeaders(publisher.AccessToken, workspaceID), &avatarUpdated)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, "/api/v1/avatar-assets/"+avatarAssetID, avatarUpdated.AvatarURL)
	require.True(t, avatarUpdated.CanManage)
	require.True(t, avatarUpdated.CanPublish)
	var storedAvatarAssetID string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT avatar_asset_id FROM service_accounts WHERE workspace_id = $1 AND id = $2
`, workspaceID, direct.Account.ID).Scan(&storedAvatarAssetID))
	require.Equal(t, avatarAssetID, storedAvatarAssetID)

	response = DoJSON(t, client, http.MethodGet, managerPath, nil, WorkspaceHeaders(publisher.AccessToken, workspaceID), &managerList)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, managerList.Managers, 2)
	require.ElementsMatch(t, []string{ownerMemberID, publisherMemberID}, []string{managerList.Managers[0].MemberID, managerList.Managers[1].MemberID})
	var ownerManager managerView
	for _, manager := range managerList.Managers {
		if manager.MemberID == ownerMemberID {
			ownerManager = manager
		}
	}
	response = DoJSON(t, client, http.MethodDelete, managerPath+fmt.Sprintf("/%s?expectedVersion=%d", ownerManager.ID, ownerManager.Version), nil, WorkspaceHeaders(publisher.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusOK, response.StatusCode)
	response = DoJSON(t, client, http.MethodDelete, managerPath+fmt.Sprintf("/%s?expectedVersion=%d", addedManager.ID, addedManager.Version), nil, WorkspaceHeaders(publisher.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusConflict, response.StatusCode, "the last active manager cannot be removed")

	clientMessageID := uuid.NewString()
	var sent dto.AppMessage
	var sentRaw json.RawMessage
	response = DoJSON(t, client, http.MethodPost, server.URL+WorkspaceScopeMessagesPath(workspaceID, direct.Account.MessageScopeID), map[string]any{
		"content":         "Release 2026.7 is available",
		"clientMessageId": clientMessageID,
		"messageType":     message.MessageTypeServiceAccountPost,
		"metadata": map[string]any{
			"category":           "release",
			"serviceAccountPost": map[string]any{"title": "Release 2026.7"},
		},
	}, WorkspaceHeaders(publisher.AccessToken, workspaceID), &sentRaw)
	require.Equalf(t, http.StatusOK, response.StatusCode, "response body: %s", string(sentRaw))
	require.NoError(t, json.Unmarshal(sentRaw, &sent))
	require.NotEmpty(t, sent.ID)
	require.Equal(t, clientMessageID, sent.ClientMessageID)
	require.Equal(t, direct.Account.MessageScopeID, sent.ChannelID)
	require.Equal(t, serviceaccount.ServiceKind, sent.ChannelType)
	require.Equal(t, message.MessageTypeServiceAccountPost, sent.MessageType)
	require.NotNil(t, sent.AuthorProfile)
	require.Equal(t, message.AuthorActorTypeService, sent.AuthorProfile.ActorType)
	require.Equal(t, message.AuthorServiceKindServiceAccount, sent.AuthorProfile.ServiceKind)
	require.Equal(t, authorMemberID, sent.AuthorProfile.MemberID)
	require.NotNil(t, sent.Source)
	require.Equal(t, message.ContributionSourceKindServiceDirectPost, sent.Source.Kind)
	require.Equal(t, sent.ID, sent.Source.RefID)

	require.Eventually(t, func() bool {
		ownerUnread := runtime.UnreadRuntime.ScopeUnreadCounts(workspaceID, ownerMemberID, []string{direct.Account.MessageScopeID})
		publisherUnread := runtime.UnreadRuntime.ScopeUnreadCounts(workspaceID, publisherMemberID, []string{direct.Account.MessageScopeID})
		return ownerUnread[direct.Account.MessageScopeID] == 1 && publisherUnread[direct.Account.MessageScopeID] == 1
	}, 5*time.Second, 25*time.Millisecond, "all Human audience members, including the publisher, receive normal unread accounting")
	ownerUnreadConversationCountBeforeDelete := runtime.UnreadRuntime.WorkspaceUnreadConversationCount(workspaceID, ownerMemberID)
	publisherUnreadConversationCountBeforeDelete := runtime.UnreadRuntime.WorkspaceUnreadConversationCount(workspaceID, publisherMemberID)
	require.Positive(t, ownerUnreadConversationCountBeforeDelete)
	require.Positive(t, publisherUnreadConversationCountBeforeDelete)

	var notificationSettingCount int64
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM scope_notification_settings
WHERE scope_id = $1 AND member_id IN ($2, $3) AND state = 'active'
`, direct.Account.MessageScopeID, ownerMemberID, publisherMemberID).Scan(&notificationSettingCount))
	require.Equal(t, int64(2), notificationSettingCount)
	var fanoutStatus string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `SELECT status FROM scope_fanout_outbox WHERE message_id = $1`, sent.ID).Scan(&fanoutStatus))
	require.Equal(t, "delivered", fanoutStatus)
	var indexedConversations int64
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM im_conversation_index
WHERE workspace_id = $1 AND scope_id = $2
`, workspaceID, direct.Account.MessageScopeID).Scan(&indexedConversations))
	require.Positive(t, indexedConversations)
	var indexedConversationPage dto.ConversationPage
	response = DoJSON(t, client, http.MethodGet, server.URL+fmt.Sprintf("/api/v1/workspaces/%s/conversations?source=index&kinds=serviceAccount&limit=100", workspaceID), nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &indexedConversationPage)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, indexedConversationPage.Items, 1)
	require.Equal(t, "serviceAccount:"+direct.Account.MessageScopeID, indexedConversationPage.Items[0].ID)
	require.Equal(t, direct.Account.MessageScopeID, indexedConversationPage.Items[0].ScopeID)
	require.Equal(t, "serviceAccount", indexedConversationPage.Items[0].Kind)

	var history dto.AppMessagePage
	response = DoJSON(t, client, http.MethodGet, server.URL+WorkspaceScopeMessagesPath(workspaceID, direct.Account.MessageScopeID), nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &history)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, history.Messages, 1)
	require.Equal(t, sent.ID, history.Messages[0].ID)
	require.Equal(t, message.AuthorServiceKindServiceAccount, history.Messages[0].AuthorProfile.ServiceKind)
	require.Contains(t, history.Messages[0].Metadata, "serviceAccountPublisher", "a workspace owner shares the manager view of the Human publisher byline")
	require.NotContains(t, history.Messages[0].Metadata, "platformServiceAccountBroadcast", "a non-manager Reader must not receive platform operator metadata")

	response = DoJSON(t, client, http.MethodDelete, accountURL+fmt.Sprintf(
		"?expectedVersion=%d",
		avatarUpdated.Version+1,
	), nil, WorkspaceHeaders(owner.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusConflict, response.StatusCode, "direct delete must honor the account version fence")
	var deleted accountView
	response = DoJSON(t, client, http.MethodDelete, accountURL+fmt.Sprintf(
		"?expectedVersion=%d",
		avatarUpdated.Version,
	), nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &deleted)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, direct.Account.ID, deleted.ID)
	response = DoJSON(t, client, http.MethodGet, accountURL, nil, WorkspaceHeaders(owner.AccessToken, workspaceID), nil)
	require.Equal(t, http.StatusNotFound, response.StatusCode)
	var accountList struct {
		ServiceAccounts []accountView `json:"serviceAccounts"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+fmt.Sprintf("/api/v1/workspaces/%s/service-accounts", workspaceID), nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &accountList)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Empty(t, accountList.ServiceAccounts)
	for table, query := range map[string]string{
		"account":      `SELECT COUNT(*) FROM service_accounts WHERE id = $1`,
		"request":      `SELECT COUNT(*) FROM service_account_create_requests WHERE created_service_account_id = $1`,
		"scope":        `SELECT COUNT(*) FROM scopes WHERE id = $1`,
		"message":      `SELECT COUNT(*) FROM scope_messages WHERE id = $1`,
		"author":       `SELECT COUNT(*) FROM members WHERE id = $1`,
		"installation": `SELECT COUNT(*) FROM service_installations WHERE id = $1`,
		"avatar":       `SELECT COUNT(*) FROM avatars WHERE owner_kind = 'service_account' AND owner_id = $1`,
	} {
		ref := direct.Account.ID
		switch table {
		case "scope":
			ref = direct.Account.MessageScopeID
		case "message":
			ref = sent.ID
		case "author":
			ref = authorMemberID
		case "installation":
			ref = serviceInstallationID
		}
		var count int64
		require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, query, ref).Scan(&count), table)
		require.Zero(t, count, table)
	}
	var deletionAudits int64
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM service_account_audit_events
WHERE event_type = 'service_account.deleted'
  AND subject_id = $1
  AND service_account_id IS NULL
`, direct.Account.ID).Scan(&deletionAudits))
	require.Equal(t, int64(1), deletionAudits)
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM im_conversation_index
WHERE workspace_id = $1 AND scope_id = $2
`, workspaceID, direct.Account.MessageScopeID).Scan(&indexedConversations))
	require.Zero(t, indexedConversations)
	indexedConversationPage = dto.ConversationPage{}
	response = DoJSON(t, client, http.MethodGet, server.URL+fmt.Sprintf("/api/v1/workspaces/%s/conversations?source=index&kinds=serviceAccount&limit=100", workspaceID), nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &indexedConversationPage)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Empty(t, indexedConversationPage.Items)
	require.Eventually(t, func() bool {
		ownerUnread := runtime.UnreadRuntime.ScopeUnreadCounts(workspaceID, ownerMemberID, []string{direct.Account.MessageScopeID})
		publisherUnread := runtime.UnreadRuntime.ScopeUnreadCounts(workspaceID, publisherMemberID, []string{direct.Account.MessageScopeID})
		return ownerUnread[direct.Account.MessageScopeID] == 0 &&
			publisherUnread[direct.Account.MessageScopeID] == 0 &&
			runtime.UnreadRuntime.WorkspaceUnreadConversationCount(workspaceID, ownerMemberID) == ownerUnreadConversationCountBeforeDelete-1 &&
			runtime.UnreadRuntime.WorkspaceUnreadConversationCount(workspaceID, publisherMemberID) == publisherUnreadConversationCountBeforeDelete-1
	}, 5*time.Second, 25*time.Millisecond, "deleted Service Account scope stops contributing to runtime unread state")
	var snapshot struct {
		UnreadCounts            map[string]int64 `json:"unreadCounts"`
		UnreadConversationCount int64            `json:"unreadConversationCount"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+fmt.Sprintf("/api/v1/workspaces/%s/snapshot", workspaceID), nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &snapshot)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotContains(t, snapshot.UnreadCounts, direct.Account.MessageScopeID)
	require.Equal(t, ownerUnreadConversationCountBeforeDelete-1, snapshot.UnreadConversationCount)

	var pending createResponse
	var pendingRaw json.RawMessage
	response = DoJSON(t, client, http.MethodPost, createPath, map[string]any{
		"clientRequestId":           uuid.NewString(),
		"expectedAuthorizationPath": serviceaccount.AuthorizationPathApprovalRequired,
		"name":                      "Member News",
	}, WorkspaceHeaders(publisher.AccessToken, workspaceID), &pendingRaw)
	require.Equal(t, http.StatusAccepted, response.StatusCode)
	require.NoError(t, json.Unmarshal(pendingRaw, &pending))
	var pendingObject map[string]any
	require.NoError(t, json.Unmarshal(pendingRaw, &pendingObject))
	require.NotContains(t, pendingObject, "approval")
	require.NotContains(t, pendingObject, "assignments")
	require.NotContains(t, pendingObject, "notificationsProjected")
	require.Equal(t, serviceaccount.AuthorizationPathApprovalRequired, pending.Request.AuthorizationPath)
	require.Equal(t, serviceaccount.RequestStateAwaitingAuthorization, pending.Request.BusinessState)
	require.Equal(t, publisherMemberID, pending.Request.RequestedByMemberID)
	require.Nil(t, pending.Account)
	require.NotEmpty(t, pending.Request.ApprovalInstanceID)
	approvalID := pending.Request.ApprovalInstanceID
	var approvalState, assignmentID, approverMemberID, assignmentStatus string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT instance.state, assignment.id, assignment.approver_member_id, assignment.status
FROM approval_instances instance
JOIN approval_assignments assignment ON assignment.approval_instance_id = instance.id
WHERE instance.workspace_id = $1 AND instance.id = $2
`, workspaceID, approvalID).Scan(&approvalState, &assignmentID, &approverMemberID, &assignmentStatus))
	require.Equal(t, approval.StatePending, approvalState)
	require.Equal(t, ownerMemberID, approverMemberID)
	require.Equal(t, approval.AssignmentPending, assignmentStatus)

	var cardCount int64
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT COUNT(*)
FROM service_notifications
WHERE workspace_id = $1
  AND business_object_namespace = 'agentteam.approval'
  AND business_object_id = $2
  AND content_kind = 'interactive_card'
  AND status = 'active'
`, workspaceID, approvalID).Scan(&cardCount))
	require.Equal(t, int64(2), cardCount, "one actionable reviewer card and one applicant receipt share the Approval business object")

	var reviewerCardCount, applicantCardCount int64
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT
  SUM(recipient_basis_kind = 'approval_assignment'),
  SUM(recipient_basis_kind = 'approval_applicant')
FROM service_notifications
WHERE workspace_id = $1
  AND business_object_namespace = 'agentteam.approval'
  AND business_object_id = $2
  AND content_kind = 'interactive_card'
  AND status = 'active'
`, workspaceID, approvalID).Scan(&reviewerCardCount, &applicantCardCount))
	require.Equal(t, int64(1), reviewerCardCount)
	require.Equal(t, int64(1), applicantCardCount)

	var approvalMessageID string
	require.NoError(t, runtime.Pool.QueryRow(runtime.Ctx, `
SELECT message_id
FROM service_notifications
WHERE workspace_id = $1
  AND business_object_namespace = 'agentteam.approval'
  AND business_object_id = $2
  AND content_kind = 'interactive_card'
  AND recipient_basis_kind = 'approval_assignment'
`, workspaceID, approvalID).Scan(&approvalMessageID))
	var projectedCard struct {
		NotificationID       string          `json:"notificationId"`
		NotificationRevision int64           `json:"notificationRevision"`
		MessageID            string          `json:"messageId"`
		CardRevision         int64           `json:"cardRevision"`
		RenderPayload        json.RawMessage `json:"renderPayload"`
		State                string          `json:"state"`
	}
	response = DoJSON(t, client, http.MethodGet, server.URL+fmt.Sprintf("/api/v1/workspaces/%s/messages/%s/app-notification-card", workspaceID, approvalMessageID), nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &projectedCard)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.NotEmpty(t, projectedCard.NotificationID)
	require.Equal(t, approvalMessageID, projectedCard.MessageID)
	require.Positive(t, projectedCard.NotificationRevision)
	require.Positive(t, projectedCard.CardRevision)
	require.Equal(t, "active", projectedCard.State)
	require.JSONEq(t, fmt.Sprintf(`{
		"schemaVersion":"1",
		"title":"审批服务号创建申请",
		"titleKey":"service_account_create_review",
		"status":"pending",
		"request":{"id":%q,"name":"Member News","description":"","applicantName":"service-account-publisher"},
		"actions":[
			{"actionKey":"approve","type":"callback","label":"通过","semantics":"commit"},
			{"actionKey":"reject","type":"callback","label":"拒绝","semantics":"revise","inputSchema":{"type":"object","properties":{"reason":{"type":"string","maxLength":1000}},"additionalProperties":false}}
		],
		"approval":{"id":%q,"version":1,"assignmentId":%q,"currentProcessorNames":["service-account-owner"]}
	}`, pending.Request.ID, approvalID, assignmentID), string(projectedCard.RenderPayload))

	var ownRequest requestView
	response = DoJSON(t, client, http.MethodGet, createPath+"/"+pending.Request.ID, nil, WorkspaceHeaders(publisher.AccessToken, workspaceID), &ownRequest)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Equal(t, pending.Request.ID, ownRequest.ID)
	require.Equal(t, []string{"service-account-owner"}, ownRequest.CurrentProcessorNames)
	require.Empty(t, ownRequest.ProcessorName)

	var ownerRequests struct {
		Requests []requestView `json:"requests"`
	}
	response = DoJSON(t, client, http.MethodGet, createPath, nil, WorkspaceHeaders(owner.AccessToken, workspaceID), &ownerRequests)
	require.Equal(t, http.StatusOK, response.StatusCode)
	require.Len(t, ownerRequests.Requests, 1, "deleting an account removes its fulfilled creation request")
}
