package approvalapp

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/service/team/roleproposal"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
	"github.com/tidbcloud/AgentTeam/internal/store/message"
	"github.com/tidbcloud/AgentTeam/internal/workspace/owner"
)

type failingResolvedSubjectHandler struct {
	SubjectHandler
	err error
}

func (handler failingResolvedSubjectHandler) PresentResolved(
	context.Context,
	DecisionResult,
	PresentationContext,
) (app.AppNotificationResult, error) {
	return app.AppNotificationResult{}, handler.err
}

func TestTeamRoleProposalProjectionReconcileCreatesMissingCardAndReplaysIdempotently(t *testing.T) {
	ctx := context.Background()
	db := startApprovalAppRollbackDB(t, ctx)
	now := time.Date(2026, 7, 18, 7, 0, 0, 0, time.UTC)
	workspaceID := "wsp_approval_bot_team_reconcile"
	seedApprovalAppWorkspace(t, db, workspaceID, now)

	teamID := "team_approval_bot_reconcile"
	channelID := "chn_approval_bot_reconcile"
	mustExec(t, db, `
INSERT INTO members (
 id, workspace_id, type, display_name, workspace_role, status, created_at, updated_at
) VALUES ('mem_approval_bot_team_agent',$1,'agent','Team Agent','member','active',$2,$2)
`, workspaceID, now)
	mustExec(t, db, `
INSERT INTO machines (
 id, workspace_id, created_by_user_id, name, api_key, created_at, updated_at
) VALUES (
 'mac_approval_bot_team_reconcile',$1,'usr_approval_bot_admin',
 'Approval Reconcile Machine','approval-reconcile-key',$2,$2
)
`, workspaceID, 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 (
 'agt_reconcile',$1,'mac_approval_bot_team_reconcile',
 'mem_approval_bot_team_agent','Team Agent','','codex','gpt-5',
 '',NULL,'',$2,$2
)
`, workspaceID, now)
	mustExec(t, db, `
INSERT INTO workspace_teams (
 id, workspace_id, handle, display_name, description, avatar_url, visibility,
 status, channel_id,
 created_by_member_id, created_at, updated_at, archived_at, active_handle_key
) VALUES (
 $1,$2,'approval-reconcile','Approval Reconcile','Base description','','private',
 'active',$4,'mem_approval_bot_admin',$3,$3,NULL,$5
)
`, teamID, workspaceID, now, channelID, workspaceID+":approval-reconcile")
	mustExec(t, db, `
INSERT INTO workspace_team_members (
 id, workspace_id, team_id, actor_kind, actor_id, role, role_label,
 role_overlay_prompt, role_overlay_version, wake_on_team_mention, sort_order,
 added_by_member_id, added_at, updated_at, removed_at, active_member_key,
 active_agent_team_key, active_leader_key
) VALUES (
 'wtm_reconcile',$1,$2,'agent','mem_approval_bot_team_agent','leader','',
 'Old role',1,TRUE,10,'mem_approval_bot_admin',$3,$3,NULL,
 CONCAT($2,':agent:mem_approval_bot_team_agent'),
 CONCAT($1,':agent:mem_approval_bot_team_agent'),
 CONCAT($1,':',$2)
)
`, workspaceID, teamID, now)
	mustExec(t, db, `
INSERT INTO scopes (
 id, workspace_id, type, identity_key, latest_seq, latest_global_seq,
 archived_until_seq, status, created_by_member_id, created_at, updated_at
) VALUES ($1,$2,'channel',$3,0,0,0,'active','mem_approval_bot_admin',$4,$4)
`, channelID, workspaceID, "agent-team-channel:"+teamID, now)
	mustExec(t, db, `
INSERT INTO scope_channel_profiles (
 scope_id, workspace_id, slug, display_name, description, visibility,
 subsystem, owner_member_id, is_all_channel, created_at, updated_at
) VALUES (
 $1,$2,$3,'Approval Reconcile','Base description','private',
 'team','mem_approval_bot_admin',FALSE,$4,$4
)
`, channelID, workspaceID, teamID, now)
	require.NoError(t, message.EnsureScopeMembership(
		ctx, db, channelID, "mem_approval_bot_admin", "public", "", "active", 0, 0, now,
	))

	payload := []byte(`{
		"schemaVersion":4,
		"applicant":{"agentId":"agt_reconcile","memberId":"mem_approval_bot_team_agent","teamMemberId":"wtm_reconcile"},
		"base":{"digest":"base","team":{"description":"Base description"},"members":[
			{"teamMemberId":"wtm_historical","memberName":"Historical Team Agent","roleOverlayPrompt":"Old role"}
		]},
		"proposed":{"description":"Proposed description","members":[
			{"teamMemberId":"wtm_historical","memberName":"Historical Team Agent","roleOverlayPrompt":"New role"}
		]}
	}`)
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var payloadValue any
	require.NoError(t, decoder.Decode(&payloadValue))
	canonical, err := json.Marshal(payloadValue)
	require.NoError(t, err)
	digest := sha256.Sum256(canonical)
	approvalStore := approval.New(db)
	instance, assignments, created, err := approvalStore.Create(ctx, approval.CreateParams{
		ID: "appr_approval_bot_reconcile", WorkspaceID: workspaceID,
		SubjectType: SubjectTypeTeamRoleProposal, SubjectRef: teamID, SubjectVersion: 1,
		Payload: payload, PayloadDigest: hex.EncodeToString(digest[:]),
		IdempotencyKey:    "approval-app-reconcile-submission",
		PendingKey:        "approval-app-reconcile-pending",
		ApplicantMemberID: "mem_approval_bot_team_agent", AllowAgentApplicant: true,
		ApproverMemberIDs: []string{"mem_approval_bot_admin"},
		CorrelationID:     "cor_approval_bot_reconcile", Now: now,
	})
	require.NoError(t, err)
	require.True(t, created)
	require.Len(t, assignments, 1)

	notifications := app.New(db, app.Options{})
	projector := NewProjector(notifications, NewProvisioner(db))
	require.NoError(t, projector.Register())
	subjects := NewSubjectRegistry(approvalStore)
	require.NoError(t, subjects.Register(NewTeamRoleProposalSubjectHandler(
		approvalStore, roleproposal.New(db), projector,
	)))
	subjects.Seal()
	reconciler := NewProjectionReconciler(
		approvalStore,
		subjects,
		owner.NewAllSet(),
		nil,
	)

	require.Equal(t, int64(0), rowCount(t, ctx, db, `
SELECT COUNT(*) FROM service_notifications
WHERE workspace_id=$1 AND client_notification_id=$2
`, workspaceID, teamRoleProposalClientNotificationID(instance.ID)))

	result, err := reconciler.ProcessOnce(ctx)
	require.NoError(t, err)
	require.Equal(t, 1, result.Projected)
	require.Equal(t, int64(1), rowCount(t, ctx, db, `
SELECT COUNT(*) FROM service_notifications
WHERE workspace_id=$1 AND client_notification_id=$2
`, workspaceID, teamRoleProposalClientNotificationID(instance.ID)))

	result, err = reconciler.ProcessOnce(ctx)
	require.NoError(t, err)
	require.Zero(t, result.Scanned)
	require.Equal(t, int64(1), rowCount(t, ctx, db, `
SELECT COUNT(*) FROM service_notifications
WHERE workspace_id=$1 AND client_notification_id=$2
`, workspaceID, teamRoleProposalClientNotificationID(instance.ID)))

	cancelled, err := subjects.Cancel(ctx, CancelCommand{
		Principal: SubjectPrincipal{
			ActorType: "agent", WorkspaceID: workspaceID,
			PrincipalID: "agt_reconcile", MemberID: instance.ApplicantMemberID,
		},
		ApprovalID: instance.ID, ExpectedVersion: instance.Version,
		ClientRequestID: "cancel-reconcile-proposal", Now: now.Add(time.Minute),
	})
	require.NoError(t, err)
	require.Equal(t, approval.StateCancelled, cancelled.Instance.State)
	_, err = subjects.PresentResolved(ctx, cancelled, PresentationContext{})
	require.NoError(t, err)
	converged, err := approvalStore.Load(ctx, workspaceID, instance.ID)
	require.NoError(t, err)
	require.Equal(t, cancelled.Instance.Version, converged.ProjectedVersion)
	provisioned, err := projector.provisioner.EnsureWorkspace(ctx, workspaceID, now)
	require.NoError(t, err)
	record, found, err := notifications.FindAppNotificationByClientID(
		ctx, workspaceID, provisioned.Installation.ID,
		teamRoleProposalClientNotificationID(instance.ID),
	)
	require.NoError(t, err)
	require.True(t, found)
	require.Equal(t, "agent", record.CausedByActorType)
	require.Equal(t, "agt_reconcile", record.CausedByActorID)
	require.Equal(t, app.AppNotificationStatusResolved, record.Status)
	card, err := notifications.LoadAppNotificationCardByNotificationInTx(
		ctx, db, record.ID, false,
	)
	require.NoError(t, err)
	require.Contains(t, string(card.RenderPayloadJSON), "team_role_proposal_withdrawn")
	require.Contains(t, string(card.RenderPayloadJSON), "Historical Team Agent")
}

func TestTeamRoleProposalTerminalReconcileDoesNotManufactureMissingCard(t *testing.T) {
	ctx := context.Background()
	db := startApprovalAppRollbackDB(t, ctx)
	now := time.Date(2026, 7, 18, 9, 0, 0, 0, time.UTC)
	workspaceID := "wsp_approval_bot_terminal_without_pending"
	seedApprovalAppWorkspace(t, db, workspaceID, now)
	teamID := "team_terminal_without_pending"
	channelID := "chn_terminal_without_pending"
	mustExec(t, db, `
INSERT INTO members (
 id, workspace_id, type, display_name, workspace_role, status, created_at, updated_at
) VALUES ('mem_terminal_agent',$1,'agent','Terminal Agent','member','active',$2,$2)
`, workspaceID, now)
	mustExec(t, db, `
INSERT INTO workspace_teams (
 id, workspace_id, handle, display_name, description, avatar_url, visibility,
 status, channel_id,
 created_by_member_id, created_at, updated_at, archived_at, active_handle_key
) VALUES (
 $1,$2,'terminal-without-pending','Terminal Without Pending','Base','','private',
 'active',$4,'mem_approval_bot_admin',$3,$3,NULL,$5
)
`, teamID, workspaceID, now, channelID, workspaceID+":terminal-without-pending")
	mustExec(t, db, `
INSERT INTO scopes (
 id, workspace_id, type, identity_key, latest_seq, latest_global_seq,
 archived_until_seq, status, created_by_member_id, created_at, updated_at
) VALUES ($1,$2,'channel',$3,0,0,0,'active','mem_approval_bot_admin',$4,$4)
`, channelID, workspaceID, "agent-team-channel:"+teamID, now)
	mustExec(t, db, `
INSERT INTO scope_channel_profiles (
 scope_id, workspace_id, slug, display_name, description, visibility,
 subsystem, owner_member_id, is_all_channel, created_at, updated_at
) VALUES (
 $1,$2,$3,'Terminal Without Pending','Base','private',
 'team','mem_approval_bot_admin',FALSE,$4,$4
)
`, channelID, workspaceID, teamID, now)

	payload := []byte(`{
		"schemaVersion":4,
		"applicant":{"agentId":"agt_terminal","memberId":"mem_terminal_agent","teamMemberId":"wtm_terminal"},
		"base":{"digest":"base","team":{"description":"Base"},"members":[
			{"teamMemberId":"wtm_terminal","memberName":"Terminal Agent","roleOverlayPrompt":"Old"}
		]},
		"proposed":{"description":"Changed","members":[
			{"teamMemberId":"wtm_terminal","memberName":"Terminal Agent","roleOverlayPrompt":"New"}
		]}
	}`)
	var payloadValue any
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	require.NoError(t, decoder.Decode(&payloadValue))
	canonical, err := json.Marshal(payloadValue)
	require.NoError(t, err)
	digest := sha256.Sum256(canonical)
	approvalStore := approval.New(db)
	instance, _, created, err := approvalStore.Create(ctx, approval.CreateParams{
		ID: "appr_terminal_without_pending", WorkspaceID: workspaceID,
		SubjectType: SubjectTypeTeamRoleProposal, SubjectRef: teamID, SubjectVersion: 1,
		Payload: payload, PayloadDigest: hex.EncodeToString(digest[:]),
		IdempotencyKey:    "terminal-without-pending-submit",
		PendingKey:        "terminal-without-pending-key",
		ApplicantMemberID: "mem_terminal_agent", AllowAgentApplicant: true,
		ApproverMemberIDs: []string{"mem_approval_bot_admin"},
		Now:               now,
	})
	require.NoError(t, err)
	require.True(t, created)
	cancelled, err := approvalStore.CancelByApplicant(ctx, approval.ApplicantCancelParams{
		WorkspaceID: workspaceID, ApprovalID: instance.ID,
		ActorMemberID: instance.ApplicantMemberID, ExpectedVersion: instance.Version,
		ClientCancellationID: "terminal-without-pending-cancel",
		Reason:               "withdrawn_by_leader", Now: now.Add(time.Minute),
	})
	require.NoError(t, err)

	notifications := app.New(db, app.Options{})
	projector := NewProjector(notifications, NewProvisioner(db))
	require.NoError(t, projector.Register())
	teamHandler := NewTeamRoleProposalSubjectHandler(
		approvalStore, roleproposal.New(db), projector,
	)
	failingSubjects := NewSubjectRegistry(approvalStore)
	require.NoError(t, failingSubjects.Register(failingResolvedSubjectHandler{
		SubjectHandler: teamHandler,
		err:            fmt.Errorf("injected presentation failure"),
	}))
	failingSubjects.Seal()
	_, err = failingSubjects.PresentResolved(ctx, DecisionResult{
		Instance: cancelled.Instance,
	}, PresentationContext{})
	require.ErrorContains(t, err, "injected presentation failure")
	unprojected, err := approvalStore.Load(ctx, workspaceID, instance.ID)
	require.NoError(t, err)
	require.Zero(t, unprojected.ProjectedVersion)

	subjects := NewSubjectRegistry(approvalStore)
	require.NoError(t, subjects.Register(teamHandler))
	subjects.Seal()
	reconciler := NewProjectionReconciler(
		approvalStore, subjects, owner.NewAllSet(), nil,
	)
	result, err := reconciler.ProcessOnce(ctx)
	require.NoError(t, err)
	require.Equal(t, 1, result.Projected)
	converged, err := approvalStore.Load(ctx, workspaceID, instance.ID)
	require.NoError(t, err)
	require.Equal(t, cancelled.Instance.Version, converged.ProjectedVersion)
	provisioned, err := projector.provisioner.EnsureWorkspace(ctx, workspaceID, now)
	require.NoError(t, err)
	record, found, err := notifications.FindAppNotificationByClientID(
		ctx, workspaceID, provisioned.Installation.ID,
		teamRoleProposalClientNotificationID(instance.ID),
	)
	require.NoError(t, err)
	require.False(t, found)
	require.Empty(t, record.ID)
}

func TestTeamRoleProposalProjectionReconcileIgnoresOtherSubjectTypes(t *testing.T) {
	ctx := context.Background()
	db := startApprovalAppRollbackDB(t, ctx)
	now := time.Date(2026, 7, 18, 8, 0, 0, 0, time.UTC)
	workspaceID := "wsp_approval_bot_non_team_reconcile"
	seedApprovalAppWorkspace(t, db, workspaceID, now)

	approvalStore := approval.New(db)
	instance, _, created, err := approvalStore.Create(ctx, approval.CreateParams{
		ID: "appr_approval_bot_non_team", WorkspaceID: workspaceID,
		SubjectType: SubjectTypeServiceAccountCreate, SubjectRef: "sacr_non_team",
		SubjectVersion: 1, PayloadDigest: strings.Repeat("a", 64),
		ApplicantMemberID: "mem_approval_bot_member",
		ApproverMemberIDs: []string{"mem_approval_bot_admin"},
		CorrelationID:     "cor_approval_bot_non_team", Now: now,
	})
	require.NoError(t, err)
	require.True(t, created)

	notifications := app.New(db, app.Options{})
	projector := NewProjector(notifications, NewProvisioner(db))
	require.NoError(t, projector.Register())
	subjects := NewSubjectRegistry(approvalStore)
	require.NoError(t, subjects.Register(NewTeamRoleProposalSubjectHandler(
		approvalStore, roleproposal.New(db), projector,
	)))
	subjects.Seal()
	reconciler := NewProjectionReconciler(
		approvalStore, subjects, owner.NewAllSet(), nil,
	)

	result, err := reconciler.ProcessOnce(ctx)
	require.NoError(t, err)
	require.Zero(t, result.Scanned)
	require.Equal(t, int64(0), rowCount(t, ctx, db, `
SELECT COUNT(*) FROM service_notifications
WHERE workspace_id=$1 AND business_object_id=$2
`, workspaceID, instance.ID))
}
