package approvalapp

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/api/im/fanout"
	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/service/agentapproval"
	"github.com/tidbcloud/AgentTeam/internal/store/agent"
	"github.com/tidbcloud/AgentTeam/internal/store/agentops"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	"github.com/tidbcloud/AgentTeam/internal/store/im"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/store/unread"
	unreadservice "github.com/tidbcloud/AgentTeam/internal/unread"
)

type approvalAppTestUnreadStore struct {
	store *unread.Store
}

func (adapter approvalAppTestUnreadStore) UpsertSnapshots(ctx context.Context, snapshots []unreadservice.Snapshot) error {
	return adapter.store.UpsertSnapshots(ctx, snapshots)
}

func (adapter approvalAppTestUnreadStore) UpsertTaskBarrierSnapshots(ctx context.Context, snapshots []unreadservice.Snapshot) error {
	return adapter.store.UpsertTaskBarrierSnapshots(ctx, snapshots)
}

func (adapter approvalAppTestUnreadStore) LoadSnapshotPage(ctx context.Context, workspaceID string, memberID string, scopeIDs []string, afterScopeID string, limit int) ([]unreadservice.Snapshot, error) {
	return adapter.store.LoadSnapshotPage(ctx, unread.SnapshotPageRequest{
		WorkspaceID: workspaceID, MemberID: memberID, ScopeIDs: scopeIDs,
		AfterScopeID: afterScopeID, Limit: limit,
	})
}

func (adapter approvalAppTestUnreadStore) LoadScopeRosterPage(ctx context.Context, workspaceID string, scopeID string, afterMemberID string, limit int) ([]unreadservice.RosterMember, error) {
	return adapter.store.LoadScopeRosterPage(ctx, unread.RosterPageRequest{
		WorkspaceID: workspaceID, ScopeID: scopeID,
		AfterMemberID: afterMemberID, Limit: limit,
	})
}

func newApprovalAppTestUnreadService(t *testing.T, db coredb.DB) (*unreadservice.Service, *unreadservice.Runtime) {
	t.Helper()
	runtime, err := unreadservice.New(unreadservice.Config{})
	require.NoError(t, err)
	service, err := unreadservice.NewService(runtime, approvalAppTestUnreadStore{store: unread.New(db)}, nil)
	require.NoError(t, err)
	return service, runtime
}

func TestAgentManagementPresentPendingProjectsOwnerDMCard(t *testing.T) {
	ctx := context.Background()
	now := time.Now().UTC()
	db := startApprovalAppRollbackDB(t, ctx)
	workspaceID := "wsp_agm_proj"
	seedApprovalAppWorkspace(t, db, workspaceID, now)

	ownerUserID := "usr_agm_proj_owner"
	ownerMemberID := "mem_agm_proj_owner"
	applicantAgentID := "agt_agm_proj_app"
	applicantMemberID := "mem_agm_proj_app"
	machineID := "mac_agm_proj"
	mustExec(t, db, `INSERT INTO users (id, email, name, display_name, password_hash, created_at, updated_at) VALUES ($1,$2,$3,$3,'hash',$4,$4)`, ownerUserID, ownerUserID+"@example.com", ownerUserID, now)
	mustExec(t, db, `INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at) VALUES ($1,$2,'human',$1,$3,'owner','active',$4,$4),($5,$2,'agent',$5,NULL,NULL,'active',$4,$4)`, ownerMemberID, workspaceID, ownerUserID, now, applicantMemberID)
	mustExec(t, db, `INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, created_at, updated_at) VALUES ($1,$2,$3,$1,$4,$5,$5)`, machineID, workspaceID, ownerUserID, "key-"+machineID, now)
	mustExec(t, db, `INSERT INTO agents (id, workspace_id, machine_id, member_id, name, avatar_url, runtime, model, description, auth_token_hash, activity_detail, created_at, updated_at) VALUES ($1,$2,$3,$4,$1,'','codex','gpt-5','',NULL,'',$5,$5)`, applicantAgentID, workspaceID, machineID, applicantMemberID, now)

	service := agentapproval.New(db, approval.New(db), agentops.New(db, agentops.Options{}))
	principal := agent.AuthenticatedAgent{AgentID: applicantAgentID, WorkspaceID: workspaceID, MemberID: applicantMemberID}
	submitted, err := service.Submit(ctx, principal, agentapproval.SubmitInput{ClientRequestID: "req_proj", Now: now, Payload: agentapproval.Payload{Action: agentapproval.ActionCreate, Create: &agentapproval.CreateProposal{Name: "new", MachineID: machineID, Runtime: "codex"}}})
	require.NoError(t, err)
	require.True(t, submitted.Created)
	require.Len(t, submitted.Assignments, 1)

	provisioner := NewProvisioner(db)
	provisioned, err := provisioner.EnsureWorkspace(ctx, workspaceID, now)
	require.NoError(t, err)
	dmAccess, err := message.EnsureDMThread(ctx, db, message.MemberActor{
		WorkspaceID: workspaceID,
		MemberID:    provisioned.Member.MemberID,
	}, ownerMemberID)
	require.NoError(t, err)
	dmScopeID := message.ScopeIDForThreadAccess(dmAccess)
	notifications := app.New(db, app.Options{})
	unread, unreadRuntime := newApprovalAppTestUnreadService(t, db)
	require.NoError(t, unreadRuntime.WarmScopeRoster(
		workspaceID,
		dmScopeID,
		unreadservice.ScopeTypeDM,
		[]unreadservice.RosterMember{
			{MemberID: ownerMemberID, UserID: ownerUserID, Visibility: "dm", NotificationState: "active"},
			{MemberID: provisioned.Member.MemberID, Visibility: "dm", NotificationState: "active"},
		},
	))
	notifications.SetCommittedMessageFanout(
		fanout.NewDeliveryFanout(nil, im.New(db), nil).
			WithScopeFanoutMarker(notifications).
			WithUnreadRuntime(unread),
	)
	projector := NewProjector(notifications, provisioner)
	require.NoError(t, projector.Register())
	handler := &agentManagementSubjectHandler{approvals: approval.New(db), service: service, projector: projector}
	submitResult := SubmitResult{Instance: submitted.Approval, Assignments: submitted.Assignments, Created: true, Value: submitted}

	projected, err := handler.PresentPending(ctx, submitResult)
	require.NoError(t, err)
	require.NotEmpty(t, projected.NotificationID)
	require.False(t, projected.AlreadyCreated)
	require.Equal(t, dmScopeID, projected.ScopeID)

	replayed, err := handler.PresentPending(ctx, submitResult)
	require.NoError(t, err)
	require.True(t, replayed.AlreadyCreated)
	require.Equal(t, projected.NotificationID, replayed.NotificationID)
	require.NotEmpty(t, projected.MessageID)
	require.Equal(t, projected.MessageID, replayed.MessageID)

	clientID := agentManagementReviewClientNotificationID(submitted.Approval.ID)
	var cardCount int
	require.NoError(t, db.QueryRow(ctx, `SELECT COUNT(*) FROM service_notifications WHERE workspace_id=$1 AND client_notification_id=$2`, workspaceID, clientID).Scan(&cardCount))
	require.Equal(t, 1, cardCount)
	var cardRow, messageRow int
	require.NoError(t, db.QueryRow(ctx, `SELECT COUNT(*) FROM service_notification_cards WHERE message_id=$1`, projected.MessageID).Scan(&cardRow))
	require.Equal(t, 1, cardRow)
	require.NoError(t, db.QueryRow(ctx, `SELECT COUNT(*) FROM scope_messages WHERE id=$1`, projected.MessageID).Scan(&messageRow))
	require.Equal(t, 1, messageRow)

	record, err := notifications.LoadAppNotification(ctx, workspaceID, projected.NotificationID)
	require.NoError(t, err)
	require.Equal(t, app.AppNotificationTargetHumanDM, record.TargetKind)
	require.Equal(t, ownerMemberID, record.TargetRef)
	require.NotNil(t, record.RecipientBasisKind)
	require.Equal(t, RecipientBasisAssignment, *record.RecipientBasisKind)
	require.Equal(t, submitted.Assignments[0].ID, *record.RecipientBasisRef)
	require.Equal(t, BusinessNamespace, *record.BusinessObjectNamespace)
	require.Equal(t, BusinessTypeInstance, *record.BusinessObjectType)
	require.Equal(t, submitted.Approval.ID, *record.BusinessObjectID)
	require.NotNil(t, record.BusinessObjectVersion)
	require.Equal(t, submitted.Approval.Version, *record.BusinessObjectVersion)
	require.Equal(t, int64(1), unreadRuntime.ScopeUnreadCounts(workspaceID, ownerMemberID, nil)[projected.ScopeID])
	require.Equal(t, int64(1), rowCount(t, ctx, db, `
SELECT COUNT(*)
FROM im_stream_events
WHERE workspace_id = $1
  AND scope_id = $2
  AND member_id = $3
  AND event_id = $4
`, workspaceID, projected.ScopeID, ownerMemberID, "message.created:"+projected.MessageID))
	require.Equal(t, int64(1), rowCount(t, ctx, db, `
SELECT COUNT(*)
FROM im_conversation_index
WHERE workspace_id = $1
  AND member_id = $2
  AND conversation_id = $3
  AND kind = $4
  AND JSON_UNQUOTE(JSON_EXTRACT(projection_json, '$.kind')) = 'dm'
  AND JSON_UNQUOTE(JSON_EXTRACT(projection_json, '$.title')) = 'Approval'
  AND JSON_UNQUOTE(JSON_EXTRACT(projection_json, '$.peerType')) = 'service'
`, workspaceID, ownerMemberID, "dm:"+projected.ScopeID, message.ConversationKindDM))
	require.Equal(t, int64(1), rowCount(t, ctx, db, `
SELECT COUNT(*)
FROM scope_fanout_outbox
WHERE workspace_id = $1 AND message_id = $2 AND status = 'delivered'
`, workspaceID, projected.MessageID))
}

func TestAgentManagementPresentResolvedKeepsReviewCardIdentity(t *testing.T) {
	for _, decision := range []string{
		approval.DecisionApproved,
		approval.DecisionRejected,
	} {
		t.Run(decision, func(t *testing.T) {
			ctx := context.Background()
			now := time.Now().UTC()
			db := startApprovalAppRollbackDB(t, ctx)
			workspaceID := "wsp_agm_resolved_" + decision
			seedApprovalAppWorkspace(t, db, workspaceID, now)

			ownerUserID := "usr_agm_resolved_owner_" + decision
			ownerMemberID := "mem_agm_resolved_owner_" + decision
			applicantAgentID := "agt_agm_resolved_app_" + decision
			applicantMemberID := "mem_agm_resolved_app_" + decision
			machineID := "mac_agm_resolved_" + decision
			mustExec(t, db, `INSERT INTO users (id, email, name, display_name, password_hash, created_at, updated_at) VALUES ($1,$2,$3,$3,'hash',$4,$4)`, ownerUserID, ownerUserID+"@example.com", ownerUserID, now)
			mustExec(t, db, `INSERT INTO members (id, workspace_id, type, display_name, user_id, workspace_role, status, created_at, updated_at) VALUES ($1,$2,'human',$1,$3,'owner','active',$4,$4),($5,$2,'agent',$5,NULL,NULL,'active',$4,$4)`, ownerMemberID, workspaceID, ownerUserID, now, applicantMemberID)
			mustExec(t, db, `INSERT INTO machines (id, workspace_id, created_by_user_id, name, api_key, created_at, updated_at) VALUES ($1,$2,$3,$1,$4,$5,$5)`, machineID, workspaceID, ownerUserID, "key-"+machineID, now)
			mustExec(t, db, `INSERT INTO agents (id, workspace_id, machine_id, member_id, name, avatar_url, runtime, model, description, auth_token_hash, activity_detail, created_at, updated_at) VALUES ($1,$2,$3,$4,$1,'','codex','gpt-5','',NULL,'',$5,$5)`, applicantAgentID, workspaceID, machineID, applicantMemberID, now)

			approvalStore := approval.New(db)
			service := agentapproval.New(
				db, approvalStore, agentops.New(db, agentops.Options{}),
			)
			submitted, err := service.Submit(ctx, agent.AuthenticatedAgent{
				AgentID: applicantAgentID, WorkspaceID: workspaceID,
				MemberID: applicantMemberID,
			}, agentapproval.SubmitInput{
				ClientRequestID: "req_resolved_" + decision,
				CorrelationID:   "cor_resolved_" + decision,
				Now:             now,
				Payload: agentapproval.Payload{
					Action: agentapproval.ActionCreate,
					Create: &agentapproval.CreateProposal{
						Name: "new", MachineID: machineID, Runtime: "codex",
					},
				},
			})
			require.NoError(t, err)
			require.Len(t, submitted.Assignments, 1)

			notifications := app.New(db, app.Options{})
			projector := NewProjector(notifications, NewProvisioner(db))
			require.NoError(t, projector.Register())
			handler := &agentManagementSubjectHandler{
				approvals: approvalStore, service: service, projector: projector,
			}
			pending, err := handler.PresentPending(ctx, SubmitResult{
				Instance: submitted.Approval, Assignments: submitted.Assignments,
				Created: true, Value: submitted,
			})
			require.NoError(t, err)
			pendingRecord, err := notifications.LoadAppNotification(
				ctx, workspaceID, pending.NotificationID,
			)
			require.NoError(t, err)

			decided, err := service.Decide(ctx, approval.DecideParams{
				WorkspaceID: workspaceID, ApprovalID: submitted.Approval.ID,
				AssignmentID:     submitted.Assignments[0].ID,
				ActorMemberID:    ownerMemberID,
				ExpectedVersion:  submitted.Approval.Version,
				ClientDecisionID: "action_" + decision,
				Decision:         decision, Reason: "test", Now: now.Add(time.Second),
			})
			require.NoError(t, err)
			presentation := PresentationContext{
				NotificationID: pending.NotificationID,
				MutationID:     "action_" + decision,
			}
			projected, err := handler.PresentResolved(
				ctx, DecisionResult{
					Instance: decided.Instance, Replay: decided.Replay,
				}, presentation,
			)
			require.NoError(t, err)
			require.Equal(t, pending.NotificationID, projected.NotificationID)
			require.Equal(t, pending.MessageID, projected.MessageID)
			if decision == approval.DecisionApproved {
				require.Equal(t, app.AppNotificationStatusActive, projected.Status)
			} else {
				require.Equal(t, app.AppNotificationStatusResolved, projected.Status)
			}

			card, err := notifications.LoadAppNotificationCardInTx(
				ctx, db, pending.MessageID, false,
			)
			require.NoError(t, err)
			var cardJSON struct {
				Status  string `json:"status"`
				Actions []any  `json:"actions"`
			}
			require.NoError(t, json.Unmarshal(card.RenderPayloadJSON, &cardJSON))
			require.Empty(t, cardJSON.Actions)
			if decision == approval.DecisionApproved {
				require.Equal(t, agentManagementCardStatusProcessing, cardJSON.Status)
				require.Equal(t, app.AppNotificationCardStateActive, card.State)
			} else {
				require.Equal(t, approval.StateRejected, cardJSON.Status)
				require.Equal(t, app.AppNotificationCardStateResolved, card.State)
			}

			replayed, err := handler.PresentResolved(
				ctx, DecisionResult{
					Instance: decided.Instance, Replay: true,
				}, presentation,
			)
			require.NoError(t, err)
			require.Equal(t, projected.NotificationID, replayed.NotificationID)
			require.Equal(t, projected.MessageID, replayed.MessageID)
			require.Equal(
				t, projected.NotificationRevision, replayed.NotificationRevision,
			)
			require.Equal(t, int64(1), rowCount(t, ctx, db, `
SELECT COUNT(*) FROM service_notifications
WHERE workspace_id = $1 AND client_notification_id = $2
`, workspaceID, agentManagementReviewClientNotificationID(submitted.Approval.ID)))
			require.Equal(t, pendingRecord.Revision+1, replayed.NotificationRevision)
			require.Equal(t, int64(1), rowCount(t, ctx, db, `
SELECT COUNT(*) FROM scope_messages WHERE id = $1
`, pending.MessageID))
		})
	}
}
