package approvalapp

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

	"github.com/stretchr/testify/require"

	"github.com/tidbcloud/AgentTeam/internal/coredb"
	"github.com/tidbcloud/AgentTeam/internal/service/agentapproval"
	"github.com/tidbcloud/AgentTeam/internal/store/agentops"
	"github.com/tidbcloud/AgentTeam/internal/store/app"
	"github.com/tidbcloud/AgentTeam/internal/store/approval"
)

func TestAgentManagementTerminalCardContentOrderedAndSafe(t *testing.T) {
	unsafeError := "credential=secret session=hidden input=raw payload=frozen"
	input := agentops.OperationTerminalInput{
		Operation: agentops.Operation{
			ID: 42, WorkspaceID: "wsp_1", ApprovalID: "appr_1",
			Action:  "duplicate",
			Payload: json.RawMessage(`{"credential":"must-not-leak"}`),
		},
		TerminalStatus: "partial",
		Items: []agentops.OperationItem{
			{
				ItemIndex: 0, Status: "applied",
				Input:  json.RawMessage(`{"session":"must-not-leak"}`),
				Result: json.RawMessage(`{"agentId":"agt_safe","credential":"must-not-leak"}`),
			},
			{
				ItemIndex: 1, Status: "failed",
				Error: unsafeError,
			},
		},
	}

	payload := agentapproval.Payload{
		Action: agentapproval.ActionDuplicate,
		Duplicate: &agentapproval.DuplicateProposal{
			SourceAgentID: "agt_source", Count: 2, Mode: "config_only",
		},
		Presentation: agentapproval.PresentationSnapshot{
			Agents: []agentapproval.AgentIdentitySnapshot{{
				ID: "agt_source", DisplayName: "Source Agent",
			}},
		},
	}
	content, err := agentManagementTerminalCardContent(input, payload, "asg_1", 2, "Workspace Owner")
	require.NoError(t, err)
	var card struct {
		Status   string `json:"status"`
		Actions  []any  `json:"actions"`
		Proposal struct {
			Action  string         `json:"action"`
			Summary map[string]any `json:"summary"`
		} `json:"proposal"`
		Fields []struct {
			Label string `json:"label"`
			Value string `json:"value"`
		} `json:"fields"`
		Approval map[string]any `json:"approval"`
	}
	require.NoError(t, json.Unmarshal(content.CardJSON, &card))
	require.Equal(t, "partial", card.Status)
	require.Empty(t, card.Actions)
	require.Len(t, card.Fields, 2)
	require.Equal(t, agentapproval.ActionDuplicate, card.Proposal.Action)
	require.Equal(t, "agt_source", card.Proposal.Summary["sourceAgentId"])
	require.Equal(t, []any{map[string]any{
		"id": "agt_source", "displayName": "Source Agent",
	}}, card.Proposal.Summary["agents"])
	require.Equal(t, "result_agent", card.Fields[0].Label)
	require.Equal(t, "agt_safe", card.Fields[0].Value)
	require.Equal(t, "failed_item", card.Fields[1].Label)
	require.Equal(t, "#2", card.Fields[1].Value)
	require.Equal(t, "Workspace Owner", card.Approval["processorName"])
	require.NotContains(t, string(content.CardJSON), "must-not-leak")
	require.NotContains(t, string(content.CardJSON), "credential")
	require.NotContains(t, string(content.CardJSON), "session")
	require.NotContains(t, string(content.CardJSON), "input")
	require.NotContains(t, string(content.CardJSON), "payload")
}

func TestAgentManagementTerminalCardContentPreservesDeletedAgentIdentity(t *testing.T) {
	input := agentops.OperationTerminalInput{
		Operation: agentops.Operation{
			ApprovalID: "apr_delete", Action: agentapproval.ActionDelete,
		},
		TerminalStatus: "applied",
		Items: []agentops.OperationItem{{
			ItemIndex: 0, Status: "applied",
			Result: json.RawMessage(`{"agentId":"agt_deleted"}`),
		}},
	}
	payload := agentapproval.Payload{
		Action: agentapproval.ActionDelete,
		Delete: &agentapproval.DeleteProposal{
			AgentIDs: []string{"agt_deleted"},
		},
		Presentation: agentapproval.PresentationSnapshot{
			Agents: []agentapproval.AgentIdentitySnapshot{{
				ID: "agt_deleted", DisplayName: "Researcher",
			}},
		},
	}

	content, err := agentManagementTerminalCardContent(
		input, payload, "asg_delete", 3, "Workspace Owner",
	)
	require.NoError(t, err)
	var card struct {
		Proposal struct {
			Summary map[string]any `json:"summary"`
		} `json:"proposal"`
		Fields []any `json:"fields"`
	}
	require.NoError(t, json.Unmarshal(content.CardJSON, &card))
	require.Equal(t, []any{map[string]any{
		"id": "agt_deleted", "displayName": "Researcher",
	}}, card.Proposal.Summary["agents"])
	require.Empty(t, card.Fields)
}

func TestAgentManagementTerminalObserverConvergesSameCard(t *testing.T) {
	ctx := context.Background()
	db := startApprovalAppRollbackDB(t, ctx)
	now := time.Date(2026, 8, 2, 10, 0, 0, 0, time.UTC)
	workspaceID := "wsp_agent_terminal"
	seedApprovalAppWorkspace(t, db, workspaceID, now)
	instance, assignment := seedApprovedAgentManagementApproval(
		t, ctx, db, workspaceID, now,
	)
	namespace, objectType, objectID := BusinessNamespace, BusinessTypeInstance, instance.ID
	version := instance.Version
	basisKind, basisRef := RecipientBasisAssignment, assignment.ID
	recipient := assignment.ApproverMemberID
	notificationID, messageID := "btn_agent_terminal", "msg_agent_terminal"
	notifications := &agentManagementHandlerNotifications{
		notification: app.AppNotificationRecord{
			ID: notificationID, WorkspaceID: workspaceID,
			TargetKind:        app.AppNotificationTargetHumanDM,
			RecipientMemberID: &recipient, RecipientBasisKind: &basisKind,
			RecipientBasisRef: &basisRef, MessageID: messageID,
			ContentKind:             app.AppNotificationContentInteractiveCard,
			ClientNotificationID:    agentManagementReviewClientNotificationID(instance.ID),
			BusinessObjectNamespace: &namespace, BusinessObjectType: &objectType,
			BusinessObjectID: &objectID, BusinessObjectVersion: &version,
			Status: app.AppNotificationStatusActive, Revision: 2,
		},
		card: app.AppNotificationCardRecord{
			MessageID: messageID, AppNotificationID: notificationID,
			State: app.AppNotificationCardStateActive,
			RenderPayloadJSON: json.RawMessage(fmt.Sprintf(
				`{"schemaVersion":"1","template":"approval_action","status":"processing","approval":{"id":%q,"version":2,"assignmentId":%q},"actions":[]}`,
				instance.ID, assignment.ID,
			)),
		},
	}
	observer := NewAgentManagementTerminalObserver(
		approval.New(db), NewProjector(notifications, NewProvisioner(db)),
	)
	input := agentops.OperationTerminalInput{
		Operation: agentops.Operation{
			ID: 42, WorkspaceID: workspaceID, ApprovalID: instance.ID,
			ApplicantAgentID: "agt_applicant", OwnerUserID: "usr_approval_bot_admin",
			Action: "duplicate", RequestID: "req_agent_terminal",
			Payload: instance.Payload,
		},
		TerminalStatus: "partial",
		Items: []agentops.OperationItem{
			{OperationID: 42, ItemIndex: 0, Status: "applied", Result: json.RawMessage(`{"agentId":"agt_copy"}`)},
			{OperationID: 42, ItemIndex: 1, Status: "failed", Error: "agent operation duplicate failed"},
		},
	}

	require.NoError(t, observer.ObserveOperationTerminal(ctx, input))
	require.Equal(t, app.AppNotificationMutationResolve, notifications.mutated.Kind)
	require.Equal(t, "agent-management-operation:42:terminal", notifications.mutated.ClientMutationID)
	require.Equal(t, notificationID, notifications.notification.ID)
	require.Equal(t, messageID, notifications.notification.MessageID)
	require.Equal(t, app.AppNotificationStatusResolved, notifications.notification.Status)
	require.Equal(t, app.AppNotificationCardStateResolved, notifications.card.State)
	require.Equal(t, 1, notifications.mutationCalls)

	require.NoError(t, observer.ObserveOperationTerminal(ctx, input))
	require.Equal(t, 1, notifications.mutationCalls, "resolved matching card is converged")
}

func TestAgentManagementTerminalObserverRejectsFrozenOperationPayloadMismatch(t *testing.T) {
	ctx := context.Background()
	db := startApprovalAppRollbackDB(t, ctx)
	now := time.Date(2026, 8, 2, 10, 30, 0, 0, time.UTC)
	workspaceID := "wsp_agent_terminal_payload_mismatch"
	seedApprovalAppWorkspace(t, db, workspaceID, now)
	instance, assignment := seedApprovedAgentManagementApproval(
		t, ctx, db, workspaceID, now,
	)
	namespace, objectType, objectID := BusinessNamespace, BusinessTypeInstance, instance.ID
	version := instance.Version
	basisKind, basisRef := RecipientBasisAssignment, assignment.ID
	recipient := assignment.ApproverMemberID
	notifications := &agentManagementHandlerNotifications{
		notification: app.AppNotificationRecord{
			ID: "btn_payload_mismatch", WorkspaceID: workspaceID,
			TargetKind:        app.AppNotificationTargetHumanDM,
			RecipientMemberID: &recipient, RecipientBasisKind: &basisKind,
			RecipientBasisRef: &basisRef, MessageID: "msg_payload_mismatch",
			ContentKind:             app.AppNotificationContentInteractiveCard,
			ClientNotificationID:    agentManagementReviewClientNotificationID(instance.ID),
			BusinessObjectNamespace: &namespace, BusinessObjectType: &objectType,
			BusinessObjectID: &objectID, BusinessObjectVersion: &version,
			Status: app.AppNotificationStatusActive, Revision: 2,
		},
		card: app.AppNotificationCardRecord{
			MessageID:         "msg_payload_mismatch",
			AppNotificationID: "btn_payload_mismatch",
			State:             app.AppNotificationCardStateActive,
			RenderPayloadJSON: json.RawMessage(fmt.Sprintf(
				`{"schemaVersion":"1","template":"approval_action","status":"processing","approval":{"id":%q,"version":2,"assignmentId":%q},"actions":[]}`,
				instance.ID, assignment.ID,
			)),
		},
	}
	observer := NewAgentManagementTerminalObserver(
		approval.New(db), NewProjector(notifications, NewProvisioner(db)),
	)
	mismatchedPayload := append(json.RawMessage(nil), instance.Payload...)
	mismatchedPayload = bytesReplace(
		t, mismatchedPayload, `"sourceAgentId": "agt_source"`, `"sourceAgentId": "agt_other"`,
	)

	err := observer.ObserveOperationTerminal(ctx, agentops.OperationTerminalInput{
		Operation: agentops.Operation{
			ID: 45, WorkspaceID: workspaceID, ApprovalID: instance.ID,
			ApplicantAgentID: "agt_applicant", OwnerUserID: "usr_approval_bot_admin",
			Action: agentapproval.ActionDuplicate, RequestID: "req_agent_terminal",
			Payload: mismatchedPayload,
		},
		TerminalStatus: "failed",
		Items: []agentops.OperationItem{{
			OperationID: 45, ItemIndex: 0, Status: "failed",
			Error: "credential=must-not-leak",
		}},
	})
	require.Error(t, err)
	require.Zero(t, notifications.mutationCalls)
}

func TestAgentManagementTerminalObserverAuthorityMismatchDoesNotMutate(t *testing.T) {
	ctx := context.Background()
	db := startApprovalAppRollbackDB(t, ctx)
	now := time.Date(2026, 8, 2, 11, 0, 0, 0, time.UTC)
	workspaceID := "wsp_agent_terminal_mismatch"
	seedApprovalAppWorkspace(t, db, workspaceID, now)
	instance, assignment := seedApprovedAgentManagementApproval(
		t, ctx, db, workspaceID, now,
	)
	wrongNamespace, objectType, objectID := "wrong.namespace", BusinessTypeInstance, instance.ID
	version := instance.Version
	basisKind, basisRef := RecipientBasisAssignment, assignment.ID
	recipient := assignment.ApproverMemberID
	notifications := &agentManagementHandlerNotifications{
		notification: app.AppNotificationRecord{
			ID: "btn_wrong", WorkspaceID: workspaceID,
			TargetKind:        app.AppNotificationTargetHumanDM,
			RecipientMemberID: &recipient, RecipientBasisKind: &basisKind,
			RecipientBasisRef: &basisRef, MessageID: "msg_wrong",
			ContentKind:             app.AppNotificationContentInteractiveCard,
			ClientNotificationID:    agentManagementReviewClientNotificationID(instance.ID),
			BusinessObjectNamespace: &wrongNamespace, BusinessObjectType: &objectType,
			BusinessObjectID: &objectID, BusinessObjectVersion: &version,
			Status: app.AppNotificationStatusActive, Revision: 2,
		},
	}
	observer := NewAgentManagementTerminalObserver(
		approval.New(db), NewProjector(notifications, NewProvisioner(db)),
	)

	err := observer.ObserveOperationTerminal(ctx, agentops.OperationTerminalInput{
		Operation: agentops.Operation{
			ID: 43, WorkspaceID: workspaceID, ApprovalID: instance.ID,
			ApplicantAgentID: "agt_applicant", OwnerUserID: "usr_approval_bot_admin",
			Action: "duplicate", RequestID: "req_agent_terminal",
			Payload: instance.Payload,
		},
		TerminalStatus: "failed",
		Items: []agentops.OperationItem{{
			OperationID: 43, ItemIndex: 0, Status: "failed", Error: "safe",
		}},
	})
	require.Error(t, err)
	require.Zero(t, notifications.mutationCalls)
}

func TestAgentManagementTerminalObserverRejectsFrozenOperationIdentityMismatch(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*agentops.Operation)
	}{
		{
			name: "action",
			mutate: func(operation *agentops.Operation) {
				operation.Action = agentapproval.ActionCreate
			},
		},
		{
			name: "applicant",
			mutate: func(operation *agentops.Operation) {
				operation.ApplicantAgentID = "agt_wrong"
			},
		},
		{
			name: "owner",
			mutate: func(operation *agentops.Operation) {
				operation.OwnerUserID = "usr_wrong"
			},
		},
		{
			name: "request",
			mutate: func(operation *agentops.Operation) {
				operation.RequestID = "req_wrong"
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			ctx := context.Background()
			db := startApprovalAppRollbackDB(t, ctx)
			now := time.Date(2026, 8, 2, 11, 30, 0, 0, time.UTC)
			workspaceID := "wsp_agent_terminal_frozen_" + tt.name
			seedApprovalAppWorkspace(t, db, workspaceID, now)
			instance, assignment := seedApprovedAgentManagementApproval(
				t, ctx, db, workspaceID, now,
			)
			version := instance.Version
			basisKind, basisRef := RecipientBasisAssignment, assignment.ID
			recipient := assignment.ApproverMemberID
			namespace, objectType, objectID := BusinessNamespace, BusinessTypeInstance, instance.ID
			notifications := &agentManagementHandlerNotifications{
				notification: app.AppNotificationRecord{
					ID: "btn_frozen_" + tt.name, WorkspaceID: workspaceID,
					TargetKind:        app.AppNotificationTargetHumanDM,
					RecipientMemberID: &recipient, RecipientBasisKind: &basisKind,
					RecipientBasisRef: &basisRef, MessageID: "msg_frozen_" + tt.name,
					ContentKind:             app.AppNotificationContentInteractiveCard,
					ClientNotificationID:    agentManagementReviewClientNotificationID(instance.ID),
					BusinessObjectNamespace: &namespace, BusinessObjectType: &objectType,
					BusinessObjectID: &objectID, BusinessObjectVersion: &version,
					Status: app.AppNotificationStatusActive, Revision: 2,
				},
				card: app.AppNotificationCardRecord{
					MessageID:         "msg_frozen_" + tt.name,
					AppNotificationID: "btn_frozen_" + tt.name,
					State:             app.AppNotificationCardStateActive,
					RenderPayloadJSON: json.RawMessage(fmt.Sprintf(
						`{"schemaVersion":"1","template":"approval_action","status":"processing","approval":{"id":%q,"version":2,"assignmentId":%q},"actions":[]}`,
						instance.ID, assignment.ID,
					)),
				},
			}
			observer := NewAgentManagementTerminalObserver(
				approval.New(db), NewProjector(notifications, NewProvisioner(db)),
			)
			operation := agentops.Operation{
				ID: 44, WorkspaceID: workspaceID, ApprovalID: instance.ID,
				ApplicantAgentID: "agt_applicant", OwnerUserID: "usr_approval_bot_admin",
				Action: agentapproval.ActionDuplicate, RequestID: "req_agent_terminal",
				Payload: instance.Payload,
			}
			tt.mutate(&operation)

			err := observer.ObserveOperationTerminal(ctx, agentops.OperationTerminalInput{
				Operation: operation, TerminalStatus: "failed",
				Items: []agentops.OperationItem{{
					OperationID: 44, ItemIndex: 0, Status: "failed",
					Error: "credential=must-not-leak",
				}},
			})
			require.Error(t, err)
			require.Zero(t, notifications.mutationCalls)
		})
	}
}

func bytesReplace(t *testing.T, value json.RawMessage, old, replacement string) json.RawMessage {
	t.Helper()
	replaced := strings.Replace(string(value), old, replacement, 1)
	require.NotEqual(t, string(value), replaced)
	return json.RawMessage(replaced)
}

func seedApprovedAgentManagementApproval(
	t *testing.T,
	ctx context.Context,
	db coredb.DB,
	workspaceID string,
	now time.Time,
) (approval.Instance, approval.Assignment) {
	t.Helper()
	payload, err := json.Marshal(agentapproval.Payload{
		SchemaVersion:   agentapproval.PayloadSchemaVersion,
		ClientRequestID: "req_agent_terminal",
		Action:          agentapproval.ActionDuplicate,
		Applicant: agentapproval.Applicant{
			AgentID: "agt_applicant", MemberID: "mem_approval_bot_member",
		},
		Owner: agentapproval.OwnerRef{
			UserID: "usr_approval_bot_admin", MemberID: "mem_approval_bot_admin",
		},
		Duplicate: &agentapproval.DuplicateProposal{
			SourceAgentID: "agt_source", Count: 2,
		},
	})
	require.NoError(t, err)
	var canonicalValue any
	decoder := json.NewDecoder(strings.NewReader(string(payload)))
	decoder.UseNumber()
	require.NoError(t, decoder.Decode(&canonicalValue))
	payload, err = json.Marshal(canonicalValue)
	require.NoError(t, err)
	digest := sha256.Sum256(payload)
	instance, assignments, created, err := approval.New(db).Create(ctx, approval.CreateParams{
		ID: "appr_agent_terminal_" + workspaceID, WorkspaceID: workspaceID,
		SubjectType: agentapproval.SubjectType, SubjectRef: "agent-terminal",
		SubjectVersion: 1, Payload: payload,
		PayloadDigest:     fmt.Sprintf("%x", digest[:]),
		PendingKey:        "agent-terminal:" + workspaceID,
		ApplicantMemberID: "mem_approval_bot_member",
		ApproverMemberIDs: []string{"mem_approval_bot_admin"},
		CorrelationID:     "cor_agent_terminal", Now: now,
	})
	require.NoError(t, err)
	require.True(t, created)
	require.Len(t, assignments, 1)
	mustExec(t, db, `
UPDATE approval_instances
SET state='approved', decision='approved',
    decided_by_member_id=$3, version=2, updated_at=$4, decided_at=$4
WHERE workspace_id=$1 AND id=$2
`, workspaceID, instance.ID, assignments[0].ApproverMemberID, now.Add(time.Second))
	mustExec(t, db, `
UPDATE approval_assignments
SET status='approved', version=2, updated_at=$3, closed_at=$3
WHERE workspace_id=$1 AND id=$2
`, workspaceID, assignments[0].ID, now.Add(time.Second))
	instance, err = approval.New(db).Load(ctx, workspaceID, instance.ID)
	require.NoError(t, err)
	return instance, assignments[0]
}
