package message_test

import (
	"context"
	"testing"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	messagedomain "hyapp/services/activity-service/internal/domain/message"
	messageservice "hyapp/services/activity-service/internal/service/message"
	"hyapp/services/activity-service/internal/testutil/mysqltest"
)

func TestInboxMessageLifecycleUsesMySQLFacts(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), "lalu")
	repository := mysqltest.NewRepository(t)
	now := time.UnixMilli(1_800_000_000_000)
	svc := messageservice.New(messageservice.Config{NodeID: "activity-a"}, repository)
	svc.SetClock(func() time.Time { return now })

	first, created, err := svc.CreateInboxMessage(ctx, messagedomain.CreateInput{
		TargetUserID:      42,
		Producer:          "wallet-service",
		ProducerEventID:   "withdraw-approved-1",
		ProducerEventType: "withdraw_approved",
		MessageType:       messagedomain.SectionSystem,
		Title:             "提现审核通过",
		Summary:           "你的提现申请已通过。",
		ActionType:        "wallet_withdraw_detail",
		ActionParam:       "withdrawal_id=wd_01",
		SentAtMS:          1_800_000_000_000,
	})
	if err != nil || !created {
		t.Fatalf("CreateInboxMessage failed: created=%v message=%+v err=%v", created, first, err)
	}
	second, created, err := svc.CreateInboxMessage(ctx, messagedomain.CreateInput{
		TargetUserID:      42,
		Producer:          "activity-service",
		ProducerEventID:   "reward-1",
		ProducerEventType: "activity_reward",
		MessageType:       messagedomain.SectionActivity,
		Title:             "活动奖励到账",
		Summary:           "你的活动奖励已发放。",
		Actions: []messagedomain.InboxAction{
			{
				Action: "dismiss",
				Label:  "Dismiss",
				Style:  "secondary",
				API: messagedomain.InboxActionAPI{
					Method: "POST",
					Path:   "/api/v1/activities/act_01/dismiss",
					Body:   map[string]any{"source": "inbox"},
				},
			},
			{
				Action: "join",
				Label:  "Join now",
				Style:  "primary",
				API: messagedomain.InboxActionAPI{
					Method: "POST",
					Path:   "/api/v1/activities/act_01/join",
					Body:   map[string]any{"campaign_id": "act_01"},
				},
			},
		},
		ActionType:  "activity_detail",
		ActionParam: "activity_id=act_01",
		SentAtMS:    1_800_000_000_100,
	})
	if err != nil || !created {
		t.Fatalf("CreateInboxMessage activity failed: created=%v message=%+v err=%v", created, second, err)
	}
	if _, created, err := svc.CreateInboxMessage(ctx, messagedomain.CreateInput{
		TargetUserID:      42,
		Producer:          "wallet-service",
		ProducerEventID:   "withdraw-approved-1",
		ProducerEventType: "withdraw_approved",
		MessageType:       messagedomain.SectionSystem,
		Title:             "提现审核通过",
		Summary:           "你的提现申请已通过。",
		ActionType:        "wallet_withdraw_detail",
		ActionParam:       "withdrawal_id=wd_01",
		SentAtMS:          1_800_000_000_000,
	}); err != nil || created {
		t.Fatalf("duplicate producer event should be idempotent: created=%v err=%v", created, err)
	}
	if _, _, err := svc.CreateInboxMessage(ctx, messagedomain.CreateInput{
		TargetUserID:    42,
		Producer:        "wallet-service",
		ProducerEventID: "withdraw-approved-1",
		MessageType:     messagedomain.SectionSystem,
		Title:           "不同标题",
		Summary:         "你的提现申请已通过。",
		ActionType:      "wallet_withdraw_detail",
		ActionParam:     "withdrawal_id=wd_01",
		SentAtMS:        1_800_000_000_000,
	}); !xerr.IsCode(err, xerr.ProducerEventConflict) {
		t.Fatalf("conflicting producer event should fail, got %v", err)
	}

	tabs, err := svc.ListMessageTabs(ctx, 42)
	if err != nil {
		t.Fatalf("ListMessageTabs failed: %v", err)
	}
	if len(tabs) != 3 || tabs[0].Section != messagedomain.SectionUser || tabs[0].Source != messagedomain.SourceTencentIMSDK || tabs[1].UnreadCount != 1 || tabs[2].UnreadCount != 1 {
		t.Fatalf("unexpected tabs: %+v", tabs)
	}

	items, next, err := svc.ListInboxMessages(ctx, 42, messagedomain.SectionSystem, 1, "")
	if err != nil || next != "" || len(items) != 1 || items[0].MessageID != first.MessageID || items[0].Read() {
		t.Fatalf("system list mismatch: items=%+v next=%q err=%v", items, next, err)
	}

	read, err := svc.MarkInboxMessageRead(ctx, 42, first.MessageID)
	if err != nil || !read.Read() || read.ReadAtMS != now.UnixMilli() {
		t.Fatalf("MarkInboxMessageRead failed: %+v err=%v", read, err)
	}
	now = now.Add(time.Minute)
	readAgain, err := svc.MarkInboxMessageRead(ctx, 42, first.MessageID)
	if err != nil || readAgain.ReadAtMS != read.ReadAtMS {
		t.Fatalf("repeat read must preserve first read time: first=%+v second=%+v err=%v", read, readAgain, err)
	}
	readCount, err := svc.MarkInboxSectionRead(ctx, 42, messagedomain.SectionActivity)
	if err != nil || readCount != 1 {
		t.Fatalf("read-all activity failed: count=%d err=%v", readCount, err)
	}
	if err := svc.DeleteInboxMessage(ctx, 99, first.MessageID); !xerr.IsCode(err, xerr.MessageNotFound) {
		t.Fatalf("other user delete must look not found, got %v", err)
	}
	if err := svc.DeleteInboxMessage(ctx, 42, first.MessageID); err != nil {
		t.Fatalf("DeleteInboxMessage failed: %v", err)
	}
	items, _, err = svc.ListInboxMessages(ctx, 42, messagedomain.SectionSystem, 20, "")
	if err != nil || len(items) != 0 {
		t.Fatalf("deleted message should not be listed: items=%+v err=%v", items, err)
	}
}

func TestInboxListCursorAndExpiredMessages(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), "lalu")
	repository := mysqltest.NewRepository(t)
	now := time.UnixMilli(1_800_000_010_000)
	svc := messageservice.New(messageservice.Config{NodeID: "activity-a"}, repository)
	svc.SetClock(func() time.Time { return now })

	for _, input := range []messagedomain.CreateInput{
		{TargetUserID: 42, Producer: "admin", ProducerEventID: "system-old", MessageType: messagedomain.SectionSystem, Title: "旧消息", Summary: "旧摘要", SentAtMS: 1_800_000_000_000},
		{TargetUserID: 42, Producer: "admin", ProducerEventID: "system-new", MessageType: messagedomain.SectionSystem, Title: "新消息", Summary: "新摘要", SentAtMS: 1_800_000_001_000},
		{TargetUserID: 42, Producer: "admin", ProducerEventID: "system-expired", MessageType: messagedomain.SectionSystem, Title: "过期消息", Summary: "过期摘要", SentAtMS: 1_800_000_000_000, ExpireAtMS: 1_800_000_005_000},
	} {
		if _, _, err := svc.CreateInboxMessage(ctx, input); err != nil {
			t.Fatalf("CreateInboxMessage failed: %+v err=%v", input, err)
		}
	}

	firstPage, next, err := svc.ListInboxMessages(ctx, 42, messagedomain.SectionSystem, 1, "")
	if err != nil || len(firstPage) != 1 || firstPage[0].Title != "新消息" || next == "" {
		t.Fatalf("first page mismatch: items=%+v next=%q err=%v", firstPage, next, err)
	}
	secondPage, next, err := svc.ListInboxMessages(ctx, 42, messagedomain.SectionSystem, 1, next)
	if err != nil || len(secondPage) != 1 || secondPage[0].Title != "旧消息" || next != "" {
		t.Fatalf("second page mismatch: items=%+v next=%q err=%v", secondPage, next, err)
	}
	if _, _, err := svc.ListInboxMessages(ctx, 42, messagedomain.SectionActivity, 1, "bad-token"); !xerr.IsCode(err, xerr.PageTokenInvalid) {
		t.Fatalf("invalid cursor should fail with PAGE_TOKEN_INVALID, got %v", err)
	}
	tabs, err := svc.ListMessageTabs(ctx, 42)
	if err != nil || tabs[1].UnreadCount != 2 {
		t.Fatalf("expired message must not count unread: tabs=%+v err=%v", tabs, err)
	}
}

func TestCreateFanoutJobIsIdempotentAndDetectsCommandConflict(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), "lalu")
	repository := mysqltest.NewRepository(t)
	svc := messageservice.New(messageservice.Config{NodeID: "activity-a"}, repository)
	svc.SetClock(func() time.Time { return time.UnixMilli(1_800_000_020_000) })

	input := messagedomain.CreateFanoutInput{
		CommandID:            "cmd-fanout-1",
		MessageType:          messagedomain.SectionActivity,
		TargetScope:          "user_ids",
		TargetFilterJSON:     `{"user_ids":[42,43]}`,
		TemplateSnapshotJSON: `{"title":"活动上线","summary":"新活动已上线"}`,
		BatchSize:            500,
		CreatedBy:            "admin:1",
	}
	job, created, err := svc.CreateFanoutJob(ctx, input)
	if err != nil || !created || job.JobID == "" || job.Status != messagedomain.FanoutStatusPending {
		t.Fatalf("CreateFanoutJob failed: created=%v job=%+v err=%v", created, job, err)
	}
	retry, created, err := svc.CreateFanoutJob(ctx, input)
	if err != nil || created || retry.JobID != job.JobID {
		t.Fatalf("retry should return existing job: created=%v retry=%+v err=%v", created, retry, err)
	}
	input.TargetFilterJSON = `{"user_ids":[99]}`
	if _, _, err := svc.CreateFanoutJob(ctx, input); !xerr.IsCode(err, xerr.RequestConflict) {
		t.Fatalf("conflicting fanout command should fail, got %v", err)
	}
}

func TestNoticeHelpersCreateInboxAndFanoutPayloads(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), "lalu")
	repository := mysqltest.NewRepository(t)
	svc := messageservice.New(messageservice.Config{NodeID: "activity-a"}, repository)
	svc.SetClock(func() time.Time { return time.UnixMilli(1_800_000_025_000) })

	notice, err := svc.CreateActivityNotice(ctx, messageservice.NoticeCommand{
		TargetUserID:      42,
		ProducerEventID:   "activity-reward-42-1",
		ProducerEventType: "activity_reward",
		AggregateType:     "activity",
		AggregateID:       "act_01",
		Title:             "活动奖励到账",
		Summary:           "你的活动奖励已发放。",
		Actions: []messagedomain.InboxAction{
			{
				Action: "dismiss",
				Label:  "Dismiss",
				Style:  "secondary",
				API: messagedomain.InboxActionAPI{
					Method: "POST",
					Path:   "/api/v1/activities/act_01/dismiss",
				},
			},
			{
				Action: "join",
				Label:  "Join now",
				Style:  "primary",
				API: messagedomain.InboxActionAPI{
					Method: "POST",
					Path:   "/api/v1/activities/act_01/join",
				},
			},
		},
		ActionType:  "activity_detail",
		ActionParam: "activity_id=act_01",
		Metadata:    map[string]any{"activity_id": "act_01"},
	})
	if err != nil || !notice.Created || notice.Message.MessageType != messagedomain.SectionActivity || notice.Message.Producer != "activity-service" || len(notice.Message.Actions) != 2 || notice.Message.Actions[1].Label != "Join now" {
		t.Fatalf("CreateActivityNotice failed: notice=%+v err=%v", notice, err)
	}

	job, created, err := svc.CreateNoticeFanout(ctx, messageservice.NoticeFanoutCommand{
		CommandID:         "admin-full-server-notice-1",
		MessageType:       messagedomain.SectionSystem,
		TargetScope:       messagedomain.TargetScopeUserIDs,
		UserIDs:           []int64{42, 43, 42, 0},
		ProducerEventType: "admin_full_server_notice",
		AggregateType:     "admin_notice",
		AggregateID:       "notice_1",
		Title:             "全服维护通知",
		Summary:           "今晚 23:00 进行短暂维护。",
		Body:              "维护期间部分功能可能短暂不可用。",
		ActionType:        "app_h5",
		ActionParam:       "https://example.com/notice",
		MetadataJSON:      `{"source":"admin"}`,
		BatchSize:         500,
		CreatedBy:         "admin:1",
	})
	if err != nil || !created || job.MessageType != messagedomain.SectionSystem || job.TargetScope != messagedomain.TargetScopeUserIDs {
		t.Fatalf("CreateNoticeFanout failed: created=%v job=%+v err=%v", created, job, err)
	}
	if job.TargetFilterJSON != `{"user_ids":[42,43]}` {
		t.Fatalf("fanout target filter should deduplicate user_ids, got %s", job.TargetFilterJSON)
	}
	if _, created, err := svc.CreateNoticeFanout(ctx, messageservice.NoticeFanoutCommand{
		CommandID:         "admin-full-server-notice-1",
		MessageType:       messagedomain.SectionSystem,
		TargetScope:       messagedomain.TargetScopeUserIDs,
		UserIDs:           []int64{42, 43, 42, 0},
		ProducerEventType: "admin_full_server_notice",
		AggregateType:     "admin_notice",
		AggregateID:       "notice_1",
		Title:             "全服维护通知",
		Summary:           "今晚 23:00 进行短暂维护。",
		Body:              "维护期间部分功能可能短暂不可用。",
		ActionType:        "app_h5",
		ActionParam:       "https://example.com/notice",
		MetadataJSON:      `{"source":"admin"}`,
		BatchSize:         500,
		CreatedBy:         "admin:1",
	}); err != nil || created {
		t.Fatalf("notice fanout duplicate should be idempotent: created=%v err=%v", created, err)
	}
}

func TestFanoutWorkerMaterializesUserIDTargetsByCursor(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), "lalu")
	repository := mysqltest.NewRepository(t)
	now := time.UnixMilli(1_800_000_030_000)
	svc := messageservice.New(messageservice.Config{NodeID: "activity-a"}, repository)
	svc.SetClock(func() time.Time { return now })

	job, created, err := svc.CreateFanoutJob(ctx, messagedomain.CreateFanoutInput{
		CommandID:            "cmd-fanout-worker-1",
		MessageType:          messagedomain.SectionActivity,
		TargetScope:          messagedomain.TargetScopeUserIDs,
		TargetFilterJSON:     `{"user_ids":[44,42,43,42]}`,
		TemplateSnapshotJSON: `{"title":"活动上线","summary":"新活动已上线","action_type":"activity_detail","action_param":"activity_id=act_01","metadata_json":{"campaign":"spring"}}`,
		BatchSize:            2,
		CreatedBy:            "admin:1",
	})
	if err != nil || !created {
		t.Fatalf("CreateFanoutJob failed: created=%v job=%+v err=%v", created, job, err)
	}

	processed, err := svc.ProcessNextFanoutJob(ctx, messageservice.FanoutWorkerOptions{WorkerID: "worker-a", BatchSize: 2, MaxRetry: 3, LockTTL: time.Minute})
	if err != nil || !processed {
		t.Fatalf("first fanout batch failed: processed=%v err=%v", processed, err)
	}
	stored, exists, err := repository.GetFanoutJobByCommand(ctx, "cmd-fanout-worker-1")
	if err != nil || !exists || stored.Status != messagedomain.FanoutStatusPending || stored.CursorUserID != 43 || stored.SuccessCount != 2 || stored.TotalCount != 2 || stored.AttemptCount != 1 {
		t.Fatalf("first batch progress mismatch: exists=%v job=%+v err=%v", exists, stored, err)
	}
	for _, userID := range []int64{42, 43} {
		items, _, err := svc.ListInboxMessages(ctx, userID, messagedomain.SectionActivity, 20, "")
		if err != nil || len(items) != 1 || items[0].Title != "活动上线" || items[0].ActionType != "activity_detail" {
			t.Fatalf("user %d inbox mismatch after first batch: items=%+v err=%v", userID, items, err)
		}
	}

	now = now.Add(time.Second)
	processed, err = svc.ProcessNextFanoutJob(ctx, messageservice.FanoutWorkerOptions{WorkerID: "worker-a", BatchSize: 2, MaxRetry: 3, LockTTL: time.Minute})
	if err != nil || !processed {
		t.Fatalf("second fanout batch failed: processed=%v err=%v", processed, err)
	}
	stored, exists, err = repository.GetFanoutJobByCommand(ctx, "cmd-fanout-worker-1")
	if err != nil || !exists || stored.Status != messagedomain.FanoutStatusComplete || stored.CursorUserID != 44 || stored.SuccessCount != 3 || stored.TotalCount != 3 || stored.AttemptCount != 2 {
		t.Fatalf("second batch progress mismatch: exists=%v job=%+v err=%v", exists, stored, err)
	}
	items, _, err := svc.ListInboxMessages(ctx, 44, messagedomain.SectionActivity, 20, "")
	if err != nil || len(items) != 1 || items[0].Summary != "新活动已上线" {
		t.Fatalf("last user inbox mismatch: items=%+v err=%v", items, err)
	}
}

func TestFanoutWorkerMaterializesAllRegisteredUsersViaTargetSource(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), "lalu")
	repository := mysqltest.NewRepository(t)
	targets := &fakeFanoutTargetSource{userIDs: []int64{42, 43}}
	now := time.UnixMilli(1_800_000_040_000)
	svc := messageservice.New(messageservice.Config{NodeID: "activity-a"}, repository, messageservice.WithTargetSource(targets))
	svc.SetClock(func() time.Time { return now })

	job, created, err := svc.CreateFanoutJob(ctx, messagedomain.CreateFanoutInput{
		CommandID:            "cmd-fanout-registered-1",
		MessageType:          messagedomain.SectionSystem,
		TargetScope:          messagedomain.TargetScopeAllRegistered,
		TargetFilterJSON:     `{}`,
		TemplateSnapshotJSON: `{"title":"系统通知","summary":"所有注册用户可见"}`,
		BatchSize:            500,
		CreatedBy:            "admin:1",
	})
	if err != nil || !created || job.TargetScope != messagedomain.TargetScopeAllRegistered {
		t.Fatalf("CreateFanoutJob failed: created=%v job=%+v err=%v", created, job, err)
	}

	processed, err := svc.ProcessNextFanoutJob(ctx, messageservice.FanoutWorkerOptions{WorkerID: "worker-a", BatchSize: 500, MaxRetry: 3, LockTTL: time.Minute})
	if err != nil || !processed {
		t.Fatalf("fanout batch failed: processed=%v err=%v", processed, err)
	}
	if len(targets.queries) != 1 || targets.queries[0].TargetScope != messagedomain.TargetScopeAllRegistered {
		t.Fatalf("target source query mismatch: %+v", targets.queries)
	}
	for _, userID := range []int64{42, 43} {
		items, _, err := svc.ListInboxMessages(ctx, userID, messagedomain.SectionSystem, 20, "")
		if err != nil || len(items) != 1 || items[0].Title != "系统通知" {
			t.Fatalf("user %d inbox mismatch: items=%+v err=%v", userID, items, err)
		}
	}
}

type fakeFanoutTargetSource struct {
	queries []messageservice.TargetQuery
	userIDs []int64
}

func (s *fakeFanoutTargetSource) ListFanoutUserIDs(_ context.Context, query messageservice.TargetQuery) ([]int64, int64, bool, error) {
	s.queries = append(s.queries, query)
	if len(s.userIDs) == 0 {
		return nil, query.CursorUserID, true, nil
	}
	nextCursor := s.userIDs[len(s.userIDs)-1]
	return append([]int64(nil), s.userIDs...), nextCursor, true, nil
}
