package service

import (
	"slices"
	"testing"
	"time"

	"google.golang.org/protobuf/proto"
	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/services/room-service/internal/room/command"
	"hyapp/services/room-service/internal/room/state"
)

func TestBuildRoomGiftSentRecordsPreservesDurableFactFields(t *testing.T) {
	now := time.Date(2026, 6, 27, 10, 0, 0, 0, time.UTC)
	cmd := command.SendGift{
		Base: command.Base{
			CommandID: "cmd-gift-fact",
			ActorID:   101,
			Room:      "room-gift-fact",
		},
		TargetType:      "user",
		TargetUserID:    202,
		TargetUserIDs:   []int64{202},
		GiftID:          "gift-rose",
		PoolID:          "pool-lucky",
		GiftCount:       3,
		SenderCountryID: 86,
		SenderRegionID:  9001,
		SenderDisplayProfile: command.GiftDisplayProfile{
			UserID:                    101,
			Username:                  " sender ",
			Avatar:                    " avatar-sender ",
			DisplayUserID:             "S101",
			PrettyDisplayUserID:       "PS101",
			EffectiveVIPLevel:         7,
			GiftBroadcastAnimationURL: "https://cdn.example/vip-gift-tray.svga",
		},
		TargetDisplayProfiles: []command.GiftDisplayProfile{{
			UserID:              202,
			Username:            " receiver ",
			Avatar:              " avatar-receiver ",
			DisplayUserID:       "R202",
			PrettyDisplayUserID: "PR202",
		}},
		RobotWalletGift:    true,
		SyntheticLuckyGift: true,
	}
	billing := &walletv1.DebitGiftResponse{
		BillingReceiptId: "receipt-target-202",
		CoinSpent:        300,
		HeatValue:        450,
		GiftTypeCode:     "lucky",
		CpRelationType:   "couple",
		GiftName:         "Rose",
		GiftIconUrl:      "https://asset/icon.png",
		GiftAnimationUrl: "https://asset/anim.svga",
		GiftEffectTypes:  []string{"room_banner", "global_broadcast"},
	}

	records, err := buildRoomGiftSentRecords("room-gift-fact", 7, now, cmd, RoomMeta{VisibleRegionID: 8008}, true, 303, []giftTargetBilling{{
		TargetUserID: 202,
		CommandID:    "cmd-gift-fact:202",
		Billing:      billing,
	}}, map[int64]int64{202: 999})
	if err != nil {
		t.Fatalf("build RoomGiftSent record failed: %v", err)
	}
	if len(records) != 1 {
		t.Fatalf("RoomGiftSent must be split per target, got %d records", len(records))
	}
	record := records[0]
	if record.EventType != "RoomGiftSent" || record.RoomID != "room-gift-fact" || record.Envelope.GetRoomVersion() != 7 {
		t.Fatalf("outbox envelope mismatch: %+v", record)
	}

	var event roomeventsv1.RoomGiftSent
	if err := proto.Unmarshal(record.Envelope.GetBody(), &event); err != nil {
		t.Fatalf("unmarshal RoomGiftSent failed: %v", err)
	}
	if event.GetSenderUserId() != 101 || event.GetTargetUserId() != 202 || event.GetGiftId() != "gift-rose" || event.GetGiftCount() != 3 {
		t.Fatalf("basic gift fields mismatch: %+v", &event)
	}
	if event.GetBillingReceiptId() != "receipt-target-202" || event.GetCoinSpent() != 300 || event.GetGiftValue() != 450 || event.GetTargetGiftValue() != 999 {
		t.Fatalf("billing fields mismatch: %+v", &event)
	}
	if event.GetVisibleRegionId() != 8008 || event.GetCountryId() != 86 || event.GetRegionId() != 9001 || event.GetCommandId() != "cmd-gift-fact:202" || !event.GetRoomLocked() || event.GetOwnerUserId() != 303 {
		t.Fatalf("routing/stat fields mismatch: %+v", &event)
	}
	if event.GetGiftTypeCode() != "lucky" || event.GetCpRelationType() != "couple" || event.GetGiftName() != "Rose" {
		t.Fatalf("gift config fields mismatch: %+v", &event)
	}
	if event.GetDisplayMode() != "" {
		t.Fatalf("single-target durable fact must keep legacy display mode empty: %+v", &event)
	}
	if !slices.Equal(event.GetGiftEffectTypes(), []string{"room_banner", "global_broadcast"}) {
		t.Fatalf("gift effect types mismatch: %+v", event.GetGiftEffectTypes())
	}
	if !event.GetIsRobotGift() || !event.GetSyntheticLuckyGift() || !event.GetRealRoomRobotGift() {
		t.Fatalf("robot/lucky flags mismatch: %+v", &event)
	}
	if event.GetSenderName() != "sender" || event.GetSenderVipLevel() != 7 || event.GetSenderGiftBroadcastAnimationUrl() != "https://cdn.example/vip-gift-tray.svga" || event.GetReceiverNickname() != "receiver" || event.GetReceiverAvatar() != "avatar-receiver" {
		t.Fatalf("display profile fields mismatch: %+v", &event)
	}
}

func TestBuildRoomGiftSentRecordsKeepsBatchDisplayFactsSplitPerTarget(t *testing.T) {
	now := time.Date(2026, 6, 27, 10, 30, 0, 0, time.UTC)
	cmd := command.SendGift{
		Base: command.Base{
			CommandID: "cmd-gift-batch-facts",
			ActorID:   101,
			Room:      "room-gift-batch-facts",
		},
		TargetType:    "user",
		TargetUserID:  202,
		TargetUserIDs: []int64{202, 203},
		GiftID:        "gift-clover",
		GiftCount:     1,
		DisplayMode:   "batch",
	}
	billing := &walletv1.DebitGiftResponse{
		BillingReceiptId: "receipt-target",
		CoinSpent:        100,
		HeatValue:        100,
	}

	records, err := buildRoomGiftSentRecords("room-gift-batch-facts", 9, now, cmd, RoomMeta{}, false, 303, []giftTargetBilling{
		{TargetUserID: 202, CommandID: "cmd-gift-batch-facts:202", Billing: billing},
		{TargetUserID: 203, CommandID: "cmd-gift-batch-facts:203", Billing: billing},
	}, nil)
	if err != nil {
		t.Fatalf("build batch RoomGiftSent records failed: %v", err)
	}
	if len(records) != 2 {
		t.Fatalf("batch gift facts must stay split per target, got %d records", len(records))
	}
	for _, record := range records {
		var event roomeventsv1.RoomGiftSent
		if err := proto.Unmarshal(record.Envelope.GetBody(), &event); err != nil {
			t.Fatalf("unmarshal batch RoomGiftSent failed: %v", err)
		}
		if event.GetTargetUserId() <= 0 || event.GetCommandId() == "" || event.GetOwnerUserId() != 303 {
			t.Fatalf("batch RoomGiftSent fact must keep per-target fields: %+v", &event)
		}
		if event.GetDisplayMode() != "batch" {
			t.Fatalf("batch RoomGiftSent fact must be marked for display suppression: %+v", &event)
		}
	}
}

func TestRoomGiftBatchSentEventFromDisplayPreservesTargetsAndTotals(t *testing.T) {
	cmd := command.SendGift{
		Base: command.Base{
			CommandID: "cmd-batch-fact",
			ActorID:   301,
			Room:      "room-batch-fact",
		},
		TargetUserID:  401,
		TargetUserIDs: []int64{401, 402},
		GiftID:        "gift-star",
		PoolID:        "pool-batch",
		GiftCount:     2,
		DisplayMode:   "batch",
		SenderDisplayProfile: command.GiftDisplayProfile{
			UserID:                    301,
			Username:                  "batch sender",
			Avatar:                    "sender-avatar",
			DisplayUserID:             "S301",
			PrettyDisplayUserID:       "PS301",
			EffectiveVIPLevel:         8,
			GiftBroadcastAnimationURL: "https://cdn.example/vip-gift-tray-batch.svga",
		},
		TargetDisplayProfiles: []command.GiftDisplayProfile{
			{UserID: 401, Username: "target-one", Avatar: "avatar-one", DisplayUserID: "T401", PrettyDisplayUserID: "PT401"},
			{UserID: 402, Username: "target-two", Avatar: "avatar-two", DisplayUserID: "T402", PrettyDisplayUserID: "PT402"},
		},
	}
	billing := &walletv1.DebitGiftResponse{
		BillingReceiptId: "receipt-batch-root",
		HeatValue:        330,
		CoinSpent:        220,
		GiftTypeCode:     "normal",
		CpRelationType:   "brother",
		GiftName:         "Star",
		GiftIconUrl:      "icon-star",
		GiftAnimationUrl: "anim-star",
		GiftEffectTypes:  []string{"room_banner"},
	}
	targetBillings := []giftTargetBilling{
		{TargetUserID: 401, CommandID: "cmd-batch-fact:401", Billing: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-401", HeatValue: 110, CoinSpent: 70}},
		{TargetUserID: 402, CommandID: "cmd-batch-fact:402", Billing: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-402", HeatValue: 220, CoinSpent: 150}},
	}
	luckyGifts := []*roomv1.LuckyGiftDrawResult{{
		Enabled:              true,
		DrawId:               "draw-402",
		CommandId:            "cmd-batch-fact:402",
		PoolId:               "pool-batch",
		GiftId:               "gift-star",
		MultiplierPpm:        2000000,
		EffectiveRewardCoins: 50,
		TargetUserId:         402,
	}}

	display := buildSendGiftBatchDisplay(cmd, billing, targetBillings, map[int64]int64{401: 1001, 402: 1002}, luckyGifts, 8888)
	event := roomGiftBatchSentEventFromDisplay(display, 6006)
	if event.GetSenderUserId() != 301 || event.GetSenderVipLevel() != 8 || display.GetSenderVipLevel() != 8 || event.GetSenderGiftBroadcastAnimationUrl() != "https://cdn.example/vip-gift-tray-batch.svga" || display.GetSenderGiftBroadcastAnimationUrl() != "https://cdn.example/vip-gift-tray-batch.svga" || event.GetGiftId() != "gift-star" || event.GetGiftCount() != 2 || event.GetCommandId() != "cmd-batch-fact" {
		t.Fatalf("batch root fields mismatch: %+v", event)
	}
	if event.GetTotalGiftValue() != 330 || event.GetTotalCoinSpent() != 220 || event.GetBillingReceiptId() != "receipt-batch-root" || event.GetRoomHeat() != 8888 {
		t.Fatalf("batch total fields mismatch: %+v", event)
	}
	if event.GetVisibleRegionId() != 6006 || !slices.Equal(event.GetTargetUserIds(), []int64{401, 402}) || !slices.Equal(event.GetGiftEffectTypes(), []string{"room_banner"}) {
		t.Fatalf("batch routing/effect fields mismatch: %+v", event)
	}
	if len(event.GetTargets()) != 2 {
		t.Fatalf("batch targets mismatch: %+v", event.GetTargets())
	}
	if target := event.GetTargets()[0]; target.GetTargetUserId() != 401 || target.GetGiftValue() != 110 || target.GetTargetGiftValue() != 1001 || target.GetBillingReceiptId() != "receipt-401" {
		t.Fatalf("first batch target mismatch: %+v", target)
	}
	if target := event.GetTargets()[1]; target.GetTargetUserId() != 402 || target.GetGiftValue() != 220 || target.GetTargetGiftValue() != 1002 || target.GetLuckyGift().GetDrawId() != "draw-402" {
		t.Fatalf("second batch target mismatch: %+v", target)
	}
}

func TestRoomHeatAndRankChangedRecordsPreserveSnapshotFields(t *testing.T) {
	now := time.Date(2026, 6, 27, 11, 0, 0, 0, time.UTC)
	heatRecord, err := buildRoomHeatChangedRecord("room-heat-rank", 12, now, 77, 1007)
	if err != nil {
		t.Fatalf("build RoomHeatChanged failed: %v", err)
	}
	var heat roomeventsv1.RoomHeatChanged
	if err := proto.Unmarshal(heatRecord.Envelope.GetBody(), &heat); err != nil {
		t.Fatalf("unmarshal RoomHeatChanged failed: %v", err)
	}
	if heatRecord.EventType != "RoomHeatChanged" || heat.GetDelta() != 77 || heat.GetCurrentHeat() != 1007 {
		t.Fatalf("heat event mismatch: record=%+v event=%+v", heatRecord, &heat)
	}

	rankRecord, err := buildRoomRankChangedRecord("room-heat-rank", 13, now, state.RankItem{UserID: 501, Score: 700, GiftValue: 700})
	if err != nil {
		t.Fatalf("build RoomRankChanged failed: %v", err)
	}
	var rank roomeventsv1.RoomRankChanged
	if err := proto.Unmarshal(rankRecord.Envelope.GetBody(), &rank); err != nil {
		t.Fatalf("unmarshal RoomRankChanged failed: %v", err)
	}
	if rankRecord.EventType != "RoomRankChanged" || rank.GetUserId() != 501 || rank.GetScore() != 700 || rank.GetGiftValue() != 700 {
		t.Fatalf("rank event mismatch: record=%+v event=%+v", rankRecord, &rank)
	}
}

func TestRoomGiftLeaderboardScopedKeyDependsOnlyOnRegion(t *testing.T) {
	start := time.Date(2026, 7, 21, 0, 0, 0, 0, time.UTC)
	global := roomGiftLeaderboardScopedKey("future-app", 0, roomGiftLeaderboardPeriodToday, start)
	regional := roomGiftLeaderboardScopedKey("future-app", 7001, roomGiftLeaderboardPeriodToday, start)
	if global != "room:gift_leaderboard:future-app:day:20260721" {
		t.Fatalf("global room leaderboard key mismatch: %s", global)
	}
	if regional != "room:gift_leaderboard:future-app:region:7001:day:20260721" {
		t.Fatalf("regional room leaderboard key must not depend on a product allowlist: %s", regional)
	}
}
