package message

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

	messagedomain "hyapp/services/activity-service/internal/domain/message"
)

func TestConfirmIMPayloadActionsExposeAppButtonLabels(t *testing.T) {
	payloadJSON, err := confirmIMPayloadJSON("cfm_1", messagedomain.RoleInvitationConfirmEvent{
		InvitationID:   901,
		InvitationType: "host",
		InviterUserID:  1001,
		TargetUserID:   2002,
	}, "Alice has invited you to join ABC guild", 1760000000000)
	if err != nil {
		t.Fatalf("confirmIMPayloadJSON failed: %v", err)
	}
	var payload struct {
		Type    string `json:"type"`
		Actions []struct {
			Action string `json:"action"`
			Label  string `json:"label"`
			Text   string `json:"text"`
			Style  string `json:"style"`
		} `json:"actions"`
	}
	if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
		t.Fatalf("payload must be valid JSON: %v", err)
	}
	if payload.Type != "im_confirm" || len(payload.Actions) != 2 {
		t.Fatalf("confirm payload shape mismatch: %+v", payload)
	}
	// Flutter 现在线上按 label 渲染按钮，text 只作为旧解析兼容字段；两个字段必须一起存在。
	if payload.Actions[0].Action != "reject" || payload.Actions[0].Label != "Reject" || payload.Actions[0].Text != "Reject" || payload.Actions[0].Style != "secondary" {
		t.Fatalf("reject action mismatch: %+v", payload.Actions[0])
	}
	if payload.Actions[1].Action != "accept" || payload.Actions[1].Label != "Accept" || payload.Actions[1].Text != "Accept" || payload.Actions[1].Style != "primary" {
		t.Fatalf("accept action mismatch: %+v", payload.Actions[1])
	}
	// 外管邀请使用独立 subtype 保护 Lalu 原有邀请状态机，但通知文案仍须
	// 呈现实际业务动作，不能退化为无上下文的通用确认提示。
	externalText := roleInvitationMessageText(messagedomain.RoleInvitationConfirmEvent{
		InvitationType: "external_host",
		InviterUserID:  1001,
		AgencyName:     "ABC",
		Inviter:        messagedomain.RoleInvitationUserSnapshot{Username: "Alice"},
	})
	if externalText != "Alice has invited you to join the ABC guild" {
		t.Fatalf("external host invitation text mismatch: %q", externalText)
	}
}

func TestExternalRoleInvitationCreatesC2CConfirmAndProcessesAppAction(t *testing.T) {
	tests := []struct {
		invitationType string
		agencyName     string
		wantText       string
		action         string
		wantStatus     string
	}{
		{
			invitationType: "external_bd",
			wantText:       "Alice has invited you to become BD",
			action:         messagedomain.ActionConfirmActionAccept,
			wantStatus:     messagedomain.ActionConfirmStatusAccepted,
		},
		{
			invitationType: "external_agency",
			agencyName:     "Team Alpha",
			wantText:       "Alice has invited you to create the Team Alpha guild",
			action:         messagedomain.ActionConfirmActionAccept,
			wantStatus:     messagedomain.ActionConfirmStatusAccepted,
		},
		{
			invitationType: "external_host",
			agencyName:     "Team Alpha",
			wantText:       "Alice has invited you to join the Team Alpha guild",
			action:         messagedomain.ActionConfirmActionReject,
			wantStatus:     messagedomain.ActionConfirmStatusRejected,
		},
	}
	for index, test := range tests {
		t.Run(test.invitationType, func(t *testing.T) {
			repository := &actionConfirmRepositoryFake{}
			roleClient := &roleInvitationClientFake{invitationType: test.invitationType}
			service := NewActionConfirm(Config{NodeID: "activity-test"}, repository, roleClient)
			service.SetClock(func() time.Time { return time.UnixMilli(1_760_000_000_000) })
			invitationID := int64(901 + index)
			event := messagedomain.RoleInvitationConfirmEvent{
				SourceEventID:  "uout-external-" + test.invitationType,
				InvitationID:   invitationID,
				InvitationType: test.invitationType,
				InviterUserID:  1001,
				TargetUserID:   2002,
				AgencyName:     test.agencyName,
				Inviter:        messagedomain.RoleInvitationUserSnapshot{Username: "Alice"},
				CreatedAtMS:    1_760_000_000_000,
				RawPayloadJSON: `{"source":"user_outbox"}`,
			}
			confirm, created, err := service.ConsumeRoleInvitationCreated(context.Background(), event)
			if err != nil {
				t.Fatalf("ConsumeRoleInvitationCreated failed: %v", err)
			}
			if !created || confirm.BusinessSubtype != test.invitationType ||
				confirm.BusinessID != strconv.FormatInt(invitationID, 10) ||
				confirm.SenderUserID != 1001 || confirm.ReceiverUserID != 2002 ||
				confirm.MessageText != test.wantText {
				t.Fatalf("external invitation confirm projection mismatch: %+v", confirm)
			}
			var payload confirmIMPayload
			if err := json.Unmarshal([]byte(repository.savedOutbox.PayloadJSON), &payload); err != nil {
				t.Fatalf("decode C2C confirm payload failed: %v", err)
			}
			if payload.Type != "im_confirm" ||
				payload.BusinessType != messagedomain.ActionConfirmBusinessRoleInvitation ||
				payload.BusinessSubtype != test.invitationType ||
				payload.SenderUserID != "1001" || payload.ReceiverUserID != "2002" ||
				payload.Msg != test.wantText || len(payload.Actions) != 2 {
				t.Fatalf("external invitation C2C payload mismatch: %+v", payload)
			}

			var processed messagedomain.ActionConfirm
			if test.action == messagedomain.ActionConfirmActionAccept {
				processed, err = service.AcceptActionConfirm(context.Background(), 2002, confirm.ConfirmID, "app-accept-"+test.invitationType)
			} else {
				processed, err = service.RejectActionConfirm(context.Background(), 2002, confirm.ConfirmID, "app-reject-"+test.invitationType, "not available")
			}
			if err != nil {
				t.Fatalf("process App confirmation failed: %v", err)
			}
			if processed.Status != test.wantStatus || processed.Action != test.action {
				t.Fatalf("App confirmation terminal state mismatch: %+v", processed)
			}
			if roleClient.lastInput.ActorUserID != 2002 ||
				roleClient.lastInput.InvitationID != invitationID ||
				roleClient.lastInput.Action != test.action {
				t.Fatalf("ProcessRoleInvitation command mismatch: %+v", roleClient.lastInput)
			}
		})
	}
}

type actionConfirmRepositoryFake struct {
	savedConfirm messagedomain.ActionConfirm
	savedOutbox  messagedomain.ActionConfirmOutbox
}

func (r *actionConfirmRepositoryFake) SaveActionConfirmWithOutbox(_ context.Context, confirm messagedomain.ActionConfirm, outbox messagedomain.ActionConfirmOutbox) (bool, messagedomain.ActionConfirm, error) {
	r.savedConfirm = confirm
	r.savedOutbox = outbox
	return true, confirm, nil
}

func (r *actionConfirmRepositoryFake) ClaimActionConfirm(_ context.Context, confirmID string, actorUserID int64, action string, commandID string, _ string, _ int64, _ int64) (messagedomain.ActionConfirm, bool, error) {
	confirm := r.savedConfirm
	if confirm.ConfirmID != confirmID || confirm.ReceiverUserID != actorUserID {
		return messagedomain.ActionConfirm{}, false, nil
	}
	confirm.Status = messagedomain.ActionConfirmStatusProcessing
	confirm.Action = action
	confirm.CommandID = commandID
	r.savedConfirm = confirm
	return confirm, true, nil
}

func (r *actionConfirmRepositoryFake) CompleteActionConfirm(_ context.Context, confirmID string, commandID string, status string, action string, processedAtMS int64) (messagedomain.ActionConfirm, error) {
	confirm := r.savedConfirm
	confirm.ConfirmID = confirmID
	confirm.CommandID = commandID
	confirm.Status = status
	confirm.Action = action
	confirm.ProcessedAtMS = processedAtMS
	r.savedConfirm = confirm
	return confirm, nil
}

func (r *actionConfirmRepositoryFake) ReleaseActionConfirm(context.Context, string, string, string, int64) error {
	return nil
}

func (r *actionConfirmRepositoryFake) BatchGetActionConfirms(context.Context, int64, []string) ([]messagedomain.ActionConfirm, error) {
	return nil, nil
}

func (r *actionConfirmRepositoryFake) ListConversationActionConfirms(context.Context, int64, int64, int, int64, string) ([]messagedomain.ActionConfirm, error) {
	return nil, nil
}

func (r *actionConfirmRepositoryFake) ClaimPendingActionConfirmOutbox(context.Context, string, int64, int64, int) ([]messagedomain.ActionConfirmOutbox, error) {
	return nil, nil
}

func (r *actionConfirmRepositoryFake) MarkActionConfirmOutboxDelivered(context.Context, string, int64) error {
	return nil
}

func (r *actionConfirmRepositoryFake) MarkActionConfirmOutboxRetryable(context.Context, string, string, int64, int64) error {
	return nil
}

type roleInvitationClientFake struct {
	invitationType string
	lastInput      ProcessRoleInvitationInput
}

func (c *roleInvitationClientFake) ProcessRoleInvitation(_ context.Context, input ProcessRoleInvitationInput) (RoleInvitationState, error) {
	c.lastInput = input
	status := messagedomain.ActionConfirmStatusAccepted
	if input.Action == messagedomain.ActionConfirmActionReject {
		status = messagedomain.ActionConfirmStatusRejected
	}
	return RoleInvitationState{
		InvitationID:   input.InvitationID,
		InvitationType: c.invitationType,
		Status:         status,
	}, nil
}

func (c *roleInvitationClientFake) GetRoleInvitation(_ context.Context, actorUserID int64, invitationID int64) (RoleInvitationState, error) {
	return RoleInvitationState{
		InvitationID:   invitationID,
		InvitationType: c.invitationType,
		Status:         messagedomain.ActionConfirmStatusPending,
	}, nil
}
