package mysql

import (
	"context"
	"fmt"
	"strings"
	"testing"
	"time"

	"hyapp/internal/testutil/mysqlschema"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	domain "hyapp/services/activity-service/internal/domain/activitytemplate"
)

func TestActivityTemplateListKeywordSearchesIDCodeAndName(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), "hyapp")
	where, args := activityTemplateListWhere(ctx, domain.ListQuery{Keyword: "acttpl_123"})
	for _, field := range []string{"t.template_id LIKE ?", "t.template_code LIKE ?", "t.name LIKE ?"} {
		if !strings.Contains(where, field) {
			t.Fatalf("keyword where clause missing %s: %s", field, where)
		}
	}
	if len(args) != 5 {
		t.Fatalf("keyword args = %v, want app_code, default archived exclusion and three LIKE args", args)
	}
	for index := 2; index < len(args); index++ {
		if args[index] != "%acttpl_123%" {
			t.Fatalf("keyword arg %d = %v", index, args[index])
		}
	}
}

func TestPrepareActivityTemplateRankSettlementSeparatesMaterializationFromExpiredJobRecovery(t *testing.T) {
	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
		DatabasePrefix: "hyapp_activity_reward_recovery_test",
	})
	repository, err := Open(context.Background(), schema.DSN)
	if err != nil {
		t.Fatalf("open repository: %v", err)
	}
	t.Cleanup(func() { _ = repository.Close() })
	ctx := appcode.WithContext(context.Background(), "hyapp")

	item := createRepositoryTemplate(t, ctx, repository, "reward_crash_recovery", false, []int64{1}, 1_000, 2_000)
	item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
		TemplateID: item.TemplateID, Status: domain.StatusPublished, ExpectedRevision: item.Revision, OperatorAdminID: 90001,
	}, 100)
	if err != nil {
		t.Fatalf("publish template: %v", err)
	}
	period := domain.RankPeriod{
		AppCode: "hyapp", TemplateID: item.TemplateID, TemplateCode: item.TemplateCode, VersionNo: item.PublishedVersion,
		BoardType: domain.BoardTypeDaily, PeriodKey: time.UnixMilli(1_000).UTC().Format("2006-01-02"),
	}
	if _, err := repository.db.ExecContext(ctx, `
		INSERT INTO activity_template_rank_snapshots (
		  app_code, template_id, template_code, version_no, board_type, period_key, rank_no,
		  user_id, score, gift_count, coin_amount, first_scored_at_ms, snapshot_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, 1, 70001, 500, 5, 500, 1100, 2100)`,
		period.AppCode, period.TemplateID, period.TemplateCode, period.VersionNo, period.BoardType, period.PeriodKey); err != nil {
		t.Fatalf("insert frozen rank: %v", err)
	}
	if _, err := repository.db.ExecContext(ctx, `
		INSERT INTO activity_template_reward_jobs (
		  app_code, reward_job_id, template_id, template_code, version_no, board_type, period_key,
		  rank_no, user_id, score, resource_group_id, wallet_command_id, status, attempt_count,
		  created_at_ms, updated_at_ms
		) VALUES (?, 'rewardjob_crashed', ?, ?, ?, ?, ?, 1, 70001, 500, 99,
		          'activity-template-rank:crashed', ?, 1, 2100, 2100)`,
		period.AppCode, period.TemplateID, period.TemplateCode, period.VersionNo, period.BoardType, period.PeriodKey,
		domain.ClaimStatusRunning); err != nil {
		t.Fatalf("insert crashed running job: %v", err)
	}
	periods, err := repository.ClaimDueActivityTemplateRankPeriods(ctx, "materializer", 200_000, time.Minute, 10)
	if err != nil {
		t.Fatalf("ClaimDueActivityTemplateRankPeriods() error = %v", err)
	}
	claimed := false
	for _, candidate := range periods {
		if candidate.TemplateID == period.TemplateID && candidate.BoardType == period.BoardType && candidate.PeriodKey == period.PeriodKey {
			claimed = true
			break
		}
	}
	if !claimed {
		t.Fatalf("daily period was not claimed for materialization: %+v", periods)
	}

	jobs, err := repository.PrepareActivityTemplateRankSettlement(ctx, period, 200_001)
	if err != nil {
		t.Fatalf("PrepareActivityTemplateRankSettlement() error = %v", err)
	}
	if len(jobs) != 0 {
		t.Fatalf("PrepareActivityTemplateRankSettlement() jobs = %+v, want materialization only", jobs)
	}
	var periodStatus string
	if err := repository.db.QueryRowContext(ctx, `
		SELECT status FROM activity_template_rank_periods
		WHERE app_code = ? AND template_id = ? AND version_no = ? AND board_type = ? AND period_key = ?`,
		period.AppCode, period.TemplateID, period.VersionNo, period.BoardType, period.PeriodKey).Scan(&periodStatus); err != nil {
		t.Fatalf("read materialized period status: %v", err)
	}
	if periodStatus != domain.PeriodStatusRewarding {
		t.Fatalf("materialized period status = %q, want rewarding", periodStatus)
	}
	jobs, err = repository.ClaimActivityTemplateRewardJobs(ctx, "recovery-worker", 200_002, time.Minute, 10)
	if err != nil {
		t.Fatalf("ClaimActivityTemplateRewardJobs() error = %v", err)
	}
	if len(jobs) != 1 || jobs[0].RewardJobID != "rewardjob_crashed" || jobs[0].Status != domain.ClaimStatusRunning || jobs[0].AttemptCount != 2 {
		t.Fatalf("expired job recovery = %+v", jobs)
	}
}

func TestActivityTemplateSettlementDiscoverySkipsFiftyBlockedPeriodsBeforePending(t *testing.T) {
	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
		DatabasePrefix: "hyapp_activity_period_starvation_test",
	})
	repository, err := Open(context.Background(), schema.DSN)
	if err != nil {
		t.Fatalf("open repository: %v", err)
	}
	t.Cleanup(func() { _ = repository.Close() })
	ctx := appcode.WithContext(context.Background(), "hyapp")
	const nowMS = int64(100_000)

	for index := 0; index < 50; index++ {
		templateID := "blocked-" + fmt.Sprintf("%02d", index)
		if _, err := repository.db.ExecContext(ctx, `
			INSERT INTO activity_template_rank_periods (
			  app_code, template_id, template_code, version_no, board_type, period_key,
			  period_start_ms, period_end_ms, settle_after_ms, leaderboard_size, status,
			  locked_by, lock_until_ms, settled_at_ms, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, 1, 'total', 'total', 1, 2, ?, 100, ?, '', 0, 0, 1, 1)`,
			"hyapp", templateID, templateID, int64(index+1), domain.PeriodStatusSettling); err != nil {
			t.Fatalf("insert blocked period %d: %v", index, err)
		}
		status := domain.ClaimStatusFailed
		nextRetryAtMS := nowMS + int64(time.Hour/time.Millisecond)
		if index%2 == 0 {
			status = domain.ClaimStatusDead
			nextRetryAtMS = 0
		}
		if _, err := repository.db.ExecContext(ctx, `
			INSERT INTO activity_template_reward_jobs (
			  app_code, reward_job_id, template_id, template_code, version_no, board_type, period_key,
			  rank_no, user_id, score, resource_group_id, wallet_command_id, status, attempt_count,
			  max_attempts, next_retry_at_ms, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, 1, 'total', 'total', 1, ?, 1, 1, ?, ?, 1, 8, ?, 1, 1)`,
			"hyapp", "job-"+templateID, templateID, templateID, int64(index+1), "wallet-"+templateID,
			status, nextRetryAtMS); err != nil {
			t.Fatalf("insert blocked job %d: %v", index, err)
		}
	}
	if _, err := repository.db.ExecContext(ctx, `
		INSERT INTO activity_template_rank_periods (
		  app_code, template_id, template_code, version_no, board_type, period_key,
		  period_start_ms, period_end_ms, settle_after_ms, leaderboard_size, status,
		  locked_by, lock_until_ms, settled_at_ms, created_at_ms, updated_at_ms
		) VALUES ('hyapp', 'pending-after-blocked', 'pending-after-blocked', 1, 'total', 'total',
		          1, 2, 1000, 100, ?, '', 0, 0, 1, 1)`, domain.PeriodStatusPending); err != nil {
		t.Fatalf("insert pending period: %v", err)
	}

	periods, err := repository.ClaimDueActivityTemplateRankPeriods(ctx, "settler", nowMS, time.Minute, 50)
	if err != nil {
		t.Fatalf("ClaimDueActivityTemplateRankPeriods() error = %v", err)
	}
	if len(periods) != 1 || periods[0].TemplateID != "pending-after-blocked" {
		t.Fatalf("discovered periods = %+v, want only pending-after-blocked", periods)
	}
	if len(periods) == 50 {
		t.Fatal("blocked settling periods must not keep settlement has_more true")
	}
}

func TestActivityTemplateListTimeFilterExcludesUnscheduledDrafts(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), "hyapp")
	where, _ := activityTemplateListWhere(ctx, domain.ListQuery{EndMS: 2_000})
	if !strings.Contains(where, "t.start_ms > 0 AND t.end_ms > t.start_ms") || !strings.Contains(where, "t.start_ms < ?") {
		t.Fatalf("end-only time filter does not require a valid scheduled period: %s", where)
	}
}

func TestActivityTemplateScopesOverlap(t *testing.T) {
	tests := []struct {
		name  string
		left  domain.Template
		right domain.Template
		want  bool
	}{
		{name: "left global", left: domain.Template{AllRegions: true}, right: domain.Template{RegionIDs: []int64{1}}, want: true},
		{name: "right global", left: domain.Template{RegionIDs: []int64{1}}, right: domain.Template{AllRegions: true}, want: true},
		{name: "shared region", left: domain.Template{RegionIDs: []int64{1, 2}}, right: domain.Template{RegionIDs: []int64{2, 3}}, want: true},
		{name: "disjoint regions", left: domain.Template{RegionIDs: []int64{1}}, right: domain.Template{RegionIDs: []int64{2}}, want: false},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := activityTemplateScopesOverlap(tt.left, tt.right); got != tt.want {
				t.Fatalf("activityTemplateScopesOverlap() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestActivityTemplateRankPeriodWatermarkIsExclusive(t *testing.T) {
	const settleAfterMS = int64(122_000)
	if accepted, reason := activityTemplateRankPeriodAcceptsEvent(domain.PeriodStatusPending, settleAfterMS, settleAfterMS-1); !accepted || reason != "" {
		t.Fatalf("event before watermark = (%v, %q), want accepted", accepted, reason)
	}
	if accepted, reason := activityTemplateRankPeriodAcceptsEvent(domain.PeriodStatusPending, settleAfterMS, settleAfterMS); accepted || reason != "settlement_window_closed" {
		t.Fatalf("event at watermark = (%v, %q), want settlement_window_closed", accepted, reason)
	}
	if accepted, reason := activityTemplateRankPeriodAcceptsEvent(domain.PeriodStatusSettling, settleAfterMS, settleAfterMS-1); accepted || reason != "rank_period_closed" {
		t.Fatalf("event for settling period = (%v, %q), want rank_period_closed", accepted, reason)
	}
	if accepted, reason := activityTemplateRankPeriodAcceptsEvent(domain.PeriodStatusRewarding, settleAfterMS, settleAfterMS-1); accepted || reason != "rank_period_closed" {
		t.Fatalf("event for rewarding period = (%v, %q), want rank_period_closed", accepted, reason)
	}
}

func TestActivityTemplateLateGiftIsAcceptedBeforeWatermarkAndRejectedAtSettlement(t *testing.T) {
	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
		DatabasePrefix: "hyapp_activity_late_gift_test",
	})
	repository, err := Open(context.Background(), schema.DSN)
	if err != nil {
		t.Fatalf("open repository: %v", err)
	}
	t.Cleanup(func() { _ = repository.Close() })
	repository.SetActivityTemplateSettlementLateness(2 * time.Minute)
	ctx := appcode.WithContext(context.Background(), "hyapp")

	item := createRepositoryTemplate(t, ctx, repository, "late_gift_watermark", false, []int64{1}, 1_000, 2_000)
	item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
		TemplateID: item.TemplateID, Status: domain.StatusPublished, ExpectedRevision: item.Revision, OperatorAdminID: 90001,
	}, 100)
	if err != nil {
		t.Fatalf("publish template: %v", err)
	}
	periodKey := time.UnixMilli(1_999).UTC().Format("2006-01-02")
	var settleAfterMS int64
	if err := repository.db.QueryRowContext(ctx, `
		SELECT settle_after_ms FROM activity_template_rank_periods
		WHERE app_code = ? AND template_id = ? AND version_no = ? AND board_type = ? AND period_key = ?`,
		"hyapp", item.TemplateID, item.PublishedVersion, domain.BoardTypeDaily, periodKey).Scan(&settleAfterMS); err != nil {
		t.Fatalf("read settlement watermark: %v", err)
	}
	if settleAfterMS != 2_000+(2*time.Minute).Milliseconds() {
		t.Fatalf("settle_after_ms = %d", settleAfterMS)
	}
	periods, err := repository.ClaimDueActivityTemplateRankPeriods(ctx, "worker-before", settleAfterMS-1, time.Minute, 10)
	if err != nil || len(periods) != 0 {
		t.Fatalf("claim before watermark = %+v, err=%v", periods, err)
	}

	accepted, err := repository.ConsumeActivityTemplateGiftEvent(ctx, domain.GiftEvent{
		EventID: "event-before-watermark", UserID: 70001, GiftID: "gift_rose", GiftCount: 1,
		CoinAmount: 100, RegionID: 1, OccurredAtMS: 1_999,
	}, settleAfterMS-1)
	if err != nil || accepted.Status != domain.EventStatusConsumed {
		t.Fatalf("consume before watermark = %+v, err=%v", accepted, err)
	}
	periods, err = repository.ClaimDueActivityTemplateRankPeriods(ctx, "worker-at", settleAfterMS, time.Minute, 10)
	if err != nil || len(periods) != 2 {
		t.Fatalf("claim at watermark = %+v, err=%v", periods, err)
	}
	late, err := repository.ConsumeActivityTemplateGiftEvent(ctx, domain.GiftEvent{
		EventID: "event-at-watermark", UserID: 70001, GiftID: "gift_rose", GiftCount: 1,
		CoinAmount: 100, RegionID: 1, OccurredAtMS: 1_999,
	}, settleAfterMS)
	if err != nil || late.Status != domain.EventStatusSkipped || late.SkipReason != "daily:settlement_window_closed;total:settlement_window_closed" {
		t.Fatalf("consume at watermark = %+v, err=%v", late, err)
	}
	var score int64
	if err := repository.db.QueryRowContext(ctx, `
		SELECT score FROM activity_template_user_total_scores
		WHERE app_code = ? AND template_id = ? AND version_no = ? AND user_id = ?`,
		"hyapp", item.TemplateID, item.PublishedVersion, 70001).Scan(&score); err != nil {
		t.Fatalf("read total score: %v", err)
	}
	if score != 100 {
		t.Fatalf("total score = %d, want only the accepted event", score)
	}
}

func TestActivityTemplateLateGiftStillUpdatesOpenTotalAfterDailySettlement(t *testing.T) {
	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
		DatabasePrefix: "hyapp_activity_split_watermark_test",
	})
	repository, err := Open(context.Background(), schema.DSN)
	if err != nil {
		t.Fatalf("open repository: %v", err)
	}
	t.Cleanup(func() { _ = repository.Close() })
	repository.SetActivityTemplateSettlementLateness(2 * time.Minute)
	ctx := appcode.WithContext(context.Background(), "hyapp")
	dayStart := time.Date(2026, 7, 14, 0, 0, 0, 0, time.UTC).UnixMilli()
	dayEnd := dayStart + int64(24*time.Hour/time.Millisecond)
	item := createRepositoryTemplate(t, ctx, repository, "split_daily_total_watermark", false, []int64{1}, dayStart, dayEnd+int64(24*time.Hour/time.Millisecond))
	item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
		TemplateID: item.TemplateID, Status: domain.StatusPublished, ExpectedRevision: item.Revision, OperatorAdminID: 90001,
	}, dayStart-1_000)
	if err != nil {
		t.Fatalf("publish template: %v", err)
	}
	processingMS := dayEnd + (2 * time.Minute).Milliseconds()
	periods, err := repository.ClaimDueActivityTemplateRankPeriods(ctx, "daily-settler", processingMS, time.Minute, 10)
	if err != nil || len(periods) != 1 || periods[0].BoardType != domain.BoardTypeDaily {
		t.Fatalf("claim first daily period = %+v, err=%v", periods, err)
	}
	result, err := repository.ConsumeActivityTemplateGiftEvent(ctx, domain.GiftEvent{
		EventID: "late-daily-open-total", UserID: 70002, GiftID: "gift_rose", GiftCount: 2,
		CoinAmount: 250, RegionID: 1, OccurredAtMS: dayEnd - 1,
	}, processingMS)
	if err != nil || result.Status != domain.EventStatusConsumed || result.SkipReason != "daily:settlement_window_closed" {
		t.Fatalf("consume split-watermark gift = %+v, err=%v", result, err)
	}
	var totalScore int64
	if err := repository.db.QueryRowContext(ctx, `
		SELECT score FROM activity_template_user_total_scores
		WHERE app_code = ? AND template_id = ? AND version_no = ? AND user_id = ?`,
		"hyapp", item.TemplateID, item.PublishedVersion, 70002).Scan(&totalScore); err != nil {
		t.Fatalf("read total score: %v", err)
	}
	if totalScore != 250 {
		t.Fatalf("total score = %d, want 250", totalScore)
	}
	var dailyRows int
	if err := repository.db.QueryRowContext(ctx, `
		SELECT COUNT(*) FROM activity_template_user_day_scores
		WHERE app_code = ? AND template_id = ? AND version_no = ? AND user_id = ?`,
		"hyapp", item.TemplateID, item.PublishedVersion, 70002).Scan(&dailyRows); err != nil || dailyRows != 0 {
		t.Fatalf("closed daily score rows = %d, err=%v", dailyRows, err)
	}
}

func TestActivityTemplateRepositoryPublishSerializesScopeOverlapAndStoresVersion(t *testing.T) {
	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
		DatabasePrefix: "hyapp_activity_template_test",
	})
	repository, err := Open(context.Background(), schema.DSN)
	if err != nil {
		t.Fatalf("open repository: %v", err)
	}
	t.Cleanup(func() { _ = repository.Close() })
	ctx := appcode.WithContext(context.Background(), "hyapp")

	first := createRepositoryTemplate(t, ctx, repository, "scope_first", false, []int64{1}, 1_000, 2_000)
	first, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
		TemplateID: first.TemplateID, Status: domain.StatusPublished, ExpectedRevision: first.Revision, OperatorAdminID: 90001,
	}, 100)
	if err != nil {
		t.Fatalf("publish first template: %v", err)
	}
	if first.PublishedVersion != 1 || first.Revision != 2 {
		t.Fatalf("first published metadata = %+v", first)
	}
	version, err := repository.GetActivityTemplateVersion(ctx, first.TemplateID, 1)
	if err != nil {
		t.Fatalf("get first version: %v", err)
	}
	if version.Snapshot.Status != domain.StatusPublished || version.Snapshot.TemplateCode != first.TemplateCode || version.Snapshot.Revision != first.Revision {
		t.Fatalf("published snapshot = %+v", version.Snapshot)
	}

	overlap := createRepositoryTemplate(t, ctx, repository, "scope_overlap", false, []int64{1}, 1_500, 2_500)
	_, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
		TemplateID: overlap.TemplateID, Status: domain.StatusPublished, ExpectedRevision: overlap.Revision, OperatorAdminID: 90001,
	}, 110)
	if !xerr.IsCode(err, xerr.Conflict) {
		t.Fatalf("overlapping same-region publish error = %v, want conflict", err)
	}

	disjoint := createRepositoryTemplate(t, ctx, repository, "scope_disjoint", false, []int64{2}, 1_500, 2_500)
	if _, err := repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
		TemplateID: disjoint.TemplateID, Status: domain.StatusPublished, ExpectedRevision: disjoint.Revision, OperatorAdminID: 90001,
	}, 120); err != nil {
		t.Fatalf("publish disjoint-region template: %v", err)
	}

	global := createRepositoryTemplate(t, ctx, repository, "scope_global", true, nil, 1_700, 1_800)
	_, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
		TemplateID: global.TemplateID, Status: domain.StatusPublished, ExpectedRevision: global.Revision, OperatorAdminID: 90001,
	}, 130)
	if !xerr.IsCode(err, xerr.Conflict) {
		t.Fatalf("global overlapping publish error = %v, want conflict", err)
	}

	// [start_ms,end_ms) 允许一个活动在另一个活动结束的同一毫秒开始。
	adjacent := createRepositoryTemplate(t, ctx, repository, "scope_adjacent", false, []int64{1}, 2_000, 3_000)
	if _, err := repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
		TemplateID: adjacent.TemplateID, Status: domain.StatusPublished, ExpectedRevision: adjacent.Revision, OperatorAdminID: 90001,
	}, 140); err != nil {
		t.Fatalf("publish adjacent template: %v", err)
	}
}

func TestActivityTemplateVersionQueryJoinsDisableAndRepublishRuntimeWindows(t *testing.T) {
	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "ACTIVITY_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     mysqlschema.InitDBPath(t, mysqlschema.CallerFile(t, 1), "..", "..", "..", "deploy", "mysql", "initdb", "001_activity_service.sql"),
		DatabasePrefix: "hyapp_activity_version_runtime_test",
	})
	repository, err := Open(context.Background(), schema.DSN)
	if err != nil {
		t.Fatalf("open repository: %v", err)
	}
	t.Cleanup(func() { _ = repository.Close() })
	ctx := appcode.WithContext(context.Background(), "hyapp")

	item := createRepositoryTemplate(t, ctx, repository, "version_runtime_window", false, []int64{1}, 100, 2_000)
	item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
		TemplateID: item.TemplateID, Status: domain.StatusPublished, ExpectedRevision: item.Revision, OperatorAdminID: 90001,
	}, 100)
	if err != nil {
		t.Fatalf("publish v1: %v", err)
	}
	item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
		TemplateID: item.TemplateID, Status: domain.StatusDisabled, ExpectedRevision: item.Revision, OperatorAdminID: 90001,
	}, 500)
	if err != nil {
		t.Fatalf("disable v1: %v", err)
	}
	item, err = repository.SetActivityTemplateStatus(ctx, domain.StatusCommand{
		TemplateID: item.TemplateID, Status: domain.StatusPublished, ExpectedRevision: item.Revision, OperatorAdminID: 90002,
	}, 600)
	if err != nil {
		t.Fatalf("republish v2: %v", err)
	}

	versions, total, err := repository.ListActivityTemplateVersions(ctx, item.TemplateID, 1, 20)
	if err != nil {
		t.Fatalf("list versions: %v", err)
	}
	if total != 2 || len(versions) != 2 {
		t.Fatalf("version total/items = %d/%d", total, len(versions))
	}
	if versions[0].VersionNo != 2 || versions[0].RuntimeFromMS != 600 || versions[0].RuntimeToMS != 0 {
		t.Fatalf("v2 runtime window = %+v", versions[0])
	}
	if versions[1].VersionNo != 1 || versions[1].RuntimeFromMS != 100 || versions[1].RuntimeToMS != 500 {
		t.Fatalf("v1 runtime window = %+v", versions[1])
	}
	v1, err := repository.GetActivityTemplateVersion(ctx, item.TemplateID, 1)
	if err != nil {
		t.Fatalf("get v1: %v", err)
	}
	if v1.RuntimeFromMS != 100 || v1.RuntimeToMS != 500 {
		t.Fatalf("get v1 runtime window = %+v", v1)
	}
}

func createRepositoryTemplate(t *testing.T, ctx context.Context, repository *Repository, code string, allRegions bool, regionIDs []int64, startMS int64, endMS int64) domain.Template {
	t.Helper()
	item, err := repository.CreateActivityTemplate(ctx, domain.Template{
		TemplateCode: code, Name: code, ActivityType: domain.ActivityTypeGiftChallenge,
		StartMS: startMS, EndMS: endMS, AllRegions: allRegions, RegionIDs: regionIDs,
		Gifts: []domain.Gift{{
			GiftID: "gift_rose", SortOrder: 1, Name: "Rose", IconURL: "https://cdn.example.com/gift_rose.png",
			CoinPrice: 100, PriceVersion: "v1",
		}},
		DailyLeaderboardSize: 100, TotalLeaderboardSize: 100, DisplayConfigJSON: `{}`,
	}, 90001, 10)
	if err != nil {
		t.Fatalf("create template %s: %v", code, err)
	}
	return item
}
