package integration

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"net/http"
	"testing"
	"time"

	roomeventsv1 "hyapp.local/api/proto/events/room/v1"
	"hyapp/pkg/tencentim"
	"hyapp/services/room-service/internal/room/outbox"
)

func TestRoomEventFromEnvelopeSkipsNonIMEvents(t *testing.T) {
	record, err := outbox.Build("room-noop", "RoomCreated", 1, time.Now(), &roomeventsv1.RoomCreated{
		OwnerUserId: 1,
		SeatCount:   4,
		Mode:        "social",
	})
	if err != nil {
		t.Fatalf("Build RoomCreated envelope failed: %v", err)
	}

	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil {
		t.Fatalf("RoomCreated should be a no-op success: %v", err)
	}
	if publish {
		t.Fatalf("RoomCreated must not publish Tencent IM message: %+v", event)
	}
}

func TestRoomUserJoinedCarriesEntryVehicleSnapshot(t *testing.T) {
	record, err := outbox.Build("room-vehicle", "RoomUserJoined", 9, time.Now(), &roomeventsv1.RoomUserJoined{
		UserId:                 42,
		Role:                   "audience",
		VisibleRegionId:        86,
		Nickname:               "Join Nina",
		Avatar:                 "https://cdn.example.com/nina.png",
		DisplayUserId:          "100042",
		PrettyDisplayUserId:    "888042",
		VipProgramType:         "tiered_privilege_v1",
		EffectiveVipLevel:      4,
		EffectiveVipName:       "VIP4",
		RoomEntryNoticeEnabled: true,
		EntryVehicle: &roomeventsv1.RoomEntryVehicleSnapshot{
			ResourceId:    7001,
			ResourceCode:  "vehicle_gold",
			Name:          "Gold Vehicle",
			AssetUrl:      "https://cdn.example.com/vehicle.png",
			PreviewUrl:    "https://cdn.example.com/vehicle-preview.png",
			AnimationUrl:  "https://cdn.example.com/vehicle.svga",
			MetadataJson:  `{"speed":"fast"}`,
			EntitlementId: "ent-vehicle",
			ExpiresAtMs:   1999999999999,
		},
	})
	if err != nil {
		t.Fatalf("Build RoomUserJoined envelope failed: %v", err)
	}

	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil {
		t.Fatalf("RoomUserJoined should decode: %v", err)
	}
	if !publish {
		t.Fatal("RoomUserJoined must publish Tencent IM message")
	}
	if event.EntryVehicle == nil || event.EntryVehicle.ResourceID != 7001 || event.EntryVehicle.AnimationURL != "https://cdn.example.com/vehicle.svga" {
		t.Fatalf("entry vehicle snapshot mismatch: %+v", event.EntryVehicle)
	}
	if event.Attributes["nickname"] != "Join Nina" || event.Attributes["avatar"] != "https://cdn.example.com/nina.png" || event.Attributes["display_user_id"] != "100042" || event.Attributes["pretty_display_user_id"] != "888042" {
		t.Fatalf("joined display profile attributes mismatch: %+v", event.Attributes)
	}
	if event.Attributes["vip_program_type"] != "tiered_privilege_v1" || event.Attributes["effective_vip_level"] != "4" || event.Attributes["effective_vip_name"] != "VIP4" || event.Attributes["room_entry_notice_enabled"] != "true" {
		t.Fatalf("joined vip attributes mismatch: %+v", event.Attributes)
	}
}

func TestRoomBackgroundChangedPublishesDedicatedIMEvent(t *testing.T) {
	record, err := outbox.Build("room-bg", "RoomBackgroundChanged", 7, time.Now(), &roomeventsv1.RoomBackgroundChanged{
		ActorUserId:       42,
		BackgroundId:      101,
		RoomBackgroundUrl: "https://cdn.example.com/bg.png",
	})
	if err != nil {
		t.Fatalf("Build RoomBackgroundChanged envelope failed: %v", err)
	}

	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil {
		t.Fatalf("RoomBackgroundChanged should decode: %v", err)
	}
	if !publish {
		t.Fatal("RoomBackgroundChanged must publish Tencent IM message")
	}
	if event.EventType != "room_background_changed" || event.Attributes["room_background_url"] != "https://cdn.example.com/bg.png" || event.Attributes["background_id"] != "101" {
		t.Fatalf("background IM event mismatch: %+v", event)
	}
}

func TestRoomBorderChangedCarriesFlatURLInIMAttributes(t *testing.T) {
	record, err := outbox.Build("room-border-im", "RoomDecorationChanged", 9, time.Now(), &roomeventsv1.RoomDecorationChanged{
		ActorUserId: 1001, DecorationType: "room_border", ResourceId: 5201, ResourceType: "room_border",
		AssetUrl: "https://cdn.example.com/room-border/gold.webp",
	})
	if err != nil {
		t.Fatalf("Build RoomDecorationChanged envelope failed: %v", err)
	}
	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil || !publish {
		t.Fatalf("room border decoration must publish: publish=%t err=%v", publish, err)
	}
	if event.EventType != "room_decoration_changed" || event.Attributes["asset_url"] != "https://cdn.example.com/room-border/gold.webp" || event.Attributes["room_border_url"] != "https://cdn.example.com/room-border/gold.webp" {
		t.Fatalf("room border IM must expose flat and generic URLs: %+v", event)
	}
}

func TestRoomGiftSentCarriesTargetGiftValueInIMAttributes(t *testing.T) {
	record, err := outbox.Build("room-gift-value", "RoomGiftSent", 8, time.Now(), &roomeventsv1.RoomGiftSent{
		SenderUserId:                    1001,
		TargetUserId:                    1002,
		GiftId:                          "gift_rose",
		GiftCount:                       1,
		GiftValue:                       117,
		TargetGiftValue:                 367,
		SenderName:                      "Robot Sender",
		SenderAvatar:                    "https://cdn.example/sender.png",
		SenderDisplayUserId:             "100001",
		SenderVipLevel:                  7,
		SenderGiftBroadcastAnimationUrl: "https://cdn.example/vip-gift-tray.svga",
		ReceiverNickname:                "Robot Receiver",
		ReceiverAvatar:                  "https://cdn.example/receiver.png",
		ReceiverDisplayUserId:           "100002",
	})
	if err != nil {
		t.Fatalf("Build RoomGiftSent envelope failed: %v", err)
	}

	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil {
		t.Fatalf("RoomGiftSent should decode: %v", err)
	}
	if !publish {
		t.Fatal("RoomGiftSent must publish Tencent IM message")
	}
	if event.EventType != "room_gift_sent" || event.GiftValue != 117 || event.Attributes["target_gift_value"] != "367" {
		t.Fatalf("gift IM event target gift value mismatch: %+v", event)
	}
	if event.Attributes["sender_name"] != "Robot Sender" || event.Attributes["sender_avatar"] != "https://cdn.example/sender.png" || event.Attributes["sender_display_user_id"] != "100001" || event.Attributes["sender_vip_level"] != "7" || event.Attributes["sender_gift_broadcast_animation_url"] != "https://cdn.example/vip-gift-tray.svga" {
		t.Fatalf("gift IM event sender display fields mismatch: %+v", event.Attributes)
	}
	if event.Attributes["receiver_nickname"] != "Robot Receiver" || event.Attributes["receiver_avatar"] != "https://cdn.example/receiver.png" || event.Attributes["receiver_display_user_id"] != "100002" {
		t.Fatalf("gift IM event receiver display fields mismatch: %+v", event.Attributes)
	}
	payload, err := json.Marshal(event)
	if err != nil {
		t.Fatalf("marshal gift IM event failed: %v", err)
	}
	var payloadData map[string]any
	if err := json.Unmarshal(payload, &payloadData); err != nil {
		t.Fatalf("decode gift IM event failed: %v payload=%s", err, payload)
	}
	if payloadData["sender_vip_level"] != "7" || payloadData["sender_gift_broadcast_animation_url"] != "https://cdn.example/vip-gift-tray.svga" {
		t.Fatalf("gift IM JSON must flatten sender VIP for Flutter: %+v", payloadData)
	}

	robotRecord, err := outbox.Build("room-robot-gift-value", "RoomGiftSent", 8, time.Now(), &roomeventsv1.RoomGiftSent{
		SenderUserId:    2001,
		TargetUserId:    2002,
		GiftId:          "gift_robot_rose",
		GiftCount:       1,
		GiftValue:       999,
		TargetGiftValue: 888,
		CoinSpent:       100,
		IsRobotGift:     true,
	})
	if err != nil {
		t.Fatalf("Build robot RoomGiftSent envelope failed: %v", err)
	}
	robotEvent, publish, err := roomEventFromEnvelope(robotRecord.Envelope)
	if err != nil || !publish {
		t.Fatalf("robot RoomGiftSent should publish: publish=%t err=%v", publish, err)
	}
	if robotEvent.GiftValue != 0 || robotEvent.Attributes["is_robot_gift"] != "true" {
		t.Fatalf("robot gift IM must expose zero business contribution: %+v", robotEvent)
	}
	if _, exists := robotEvent.Attributes["target_gift_value"]; exists {
		t.Fatalf("robot gift IM must not overwrite authoritative target value: %+v", robotEvent.Attributes)
	}
}

func TestRoomGiftSentBatchDisplayFactSkipsSingleTargetIM(t *testing.T) {
	record, err := outbox.Build("room-gift-batch-fact", "RoomGiftSent", 9, time.Now(), &roomeventsv1.RoomGiftSent{
		SenderUserId:    1001,
		TargetUserId:    1002,
		GiftId:          "gift_clover",
		GiftCount:       1,
		GiftValue:       100,
		TargetGiftValue: 900,
		DisplayMode:     "batch",
	})
	if err != nil {
		t.Fatalf("Build batch RoomGiftSent fact failed: %v", err)
	}

	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil {
		t.Fatalf("batch RoomGiftSent fact should decode without publishing: %v", err)
	}
	if publish {
		t.Fatalf("batch RoomGiftSent fact must not publish single-target IM, event=%+v", event)
	}
}

func TestRoomGiftBatchSentPublishesSingleBatchIMEvent(t *testing.T) {
	record, err := outbox.Build("room-gift-batch", "RoomGiftBatchSent", 12, time.Now(), &roomeventsv1.RoomGiftBatchSent{
		SenderUserId:                    1001,
		SenderName:                      "Batch Sender",
		SenderVipLevel:                  8,
		SenderGiftBroadcastAnimationUrl: "https://cdn.example/vip-gift-tray-batch.svga",
		GiftId:                          "gift_clover",
		GiftCount:                       2,
		TotalGiftValue:                  600,
		TotalCoinSpent:                  600,
		BillingReceiptId:                "receipt-batch",
		RoomHeat:                        6600,
		PoolId:                          "super_lucky",
		GiftEffectTypes:                 []string{"room"},
		TargetUserIds:                   []int64{1002, 1003},
		CommandId:                       "cmd-batch",
		Targets: []*roomeventsv1.RoomGiftBatchTarget{
			{
				TargetUserId:     1002,
				ReceiverNickname: "Receiver One",
				GiftValue:        300,
				TargetGiftValue:  1300,
				CommandId:        "cmd-batch:target:1002",
				LuckyGift: &roomeventsv1.RoomGiftBatchLuckyResult{
					Enabled:              true,
					DrawId:               "draw-1",
					CommandId:            "cmd-batch:target:1002",
					PoolId:               "super_lucky",
					GiftId:               "gift_clover",
					MultiplierPpm:        2_000_000,
					EffectiveRewardCoins: 200,
					RewardStatus:         "granted",
					TargetUserId:         1002,
				},
			},
			{TargetUserId: 1003, ReceiverNickname: "Receiver Two", GiftValue: 300, TargetGiftValue: 900, CommandId: "cmd-batch:target:1003"},
		},
	})
	if err != nil {
		t.Fatalf("Build RoomGiftBatchSent envelope failed: %v", err)
	}

	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil {
		t.Fatalf("RoomGiftBatchSent should decode: %v", err)
	}
	if !publish || event.EventType != "room_gift_batch_sent" || event.ActorUserID != "1001" || event.TargetUserID != "1002" || event.GiftValue != 600 || event.RoomHeat != 6600 {
		t.Fatalf("batch gift IM event mismatch: publish=%t event=%+v", publish, event)
	}
	if event.Attributes["target_user_ids"] != `["1002","1003"]` || event.Attributes["target_count"] != "2" {
		t.Fatalf("batch gift IM target attributes mismatch: %+v", event.Attributes)
	}
	if event.Attributes["gift_id"] != "gift_clover" || event.Attributes["gift_count"] != "2" || event.Attributes["command_id"] != "cmd-batch" {
		t.Fatalf("batch gift IM gift attributes mismatch: %+v", event.Attributes)
	}
	if event.Attributes["sender_vip_level"] != "8" || event.Attributes["sender_gift_broadcast_animation_url"] != "https://cdn.example/vip-gift-tray-batch.svga" {
		t.Fatalf("batch gift IM sender VIP mismatch: %+v", event.Attributes)
	}
	var targets []struct {
		TargetUserID     string `json:"target_user_id"`
		ReceiverNickname string `json:"receiver_nickname"`
		GiftValue        string `json:"gift_value"`
		TargetGiftValue  string `json:"target_gift_value"`
		CommandID        string `json:"command_id"`
	}
	if err := json.Unmarshal([]byte(event.Attributes["targets"]), &targets); err != nil {
		t.Fatalf("decode room batch IM targets failed: %v payload=%s", err, event.Attributes["targets"])
	}
	if len(targets) != 2 || targets[0].TargetUserID != "1002" || targets[0].ReceiverNickname != "Receiver One" ||
		targets[0].GiftValue != "300" || targets[0].TargetGiftValue != "1300" || targets[0].CommandID != "cmd-batch:target:1002" ||
		targets[1].TargetUserID != "1003" || targets[1].ReceiverNickname != "Receiver Two" || targets[1].TargetGiftValue != "900" {
		t.Fatalf("room batch IM target display snapshot mismatch: %+v", targets)
	}
	if _, exists := event.Attributes["lucky_gifts"]; exists {
		t.Fatalf("room batch IM must not include bulky lucky_gifts: %+v", event.Attributes)
	}
	if _, exists := event.Attributes["coin_balance_after"]; exists {
		t.Fatalf("room IM must not expose sender coin balance: %+v", event.Attributes)
	}
	payload, err := json.Marshal(event)
	if err != nil {
		t.Fatalf("marshal batch gift IM event failed: %v", err)
	}
	if len(payload) > 4096 {
		t.Fatalf("batch gift IM payload should stay small, bytes=%d payload=%s", len(payload), payload)
	}
}

func TestRoomHeatAndRankChangedPublishDisplayIMEvents(t *testing.T) {
	heatRecord, err := outbox.Build("room-robot-heat", "RoomHeatChanged", 9, time.Now(), &roomeventsv1.RoomHeatChanged{
		Delta:       120,
		CurrentHeat: 880,
	})
	if err != nil {
		t.Fatalf("Build RoomHeatChanged envelope failed: %v", err)
	}
	heatEvent, publish, err := roomEventFromEnvelope(heatRecord.Envelope)
	if err != nil {
		t.Fatalf("RoomHeatChanged should decode: %v", err)
	}
	if !publish || heatEvent.EventType != "room_heat_changed" || heatEvent.RoomHeat != 880 || heatEvent.Attributes["delta"] != "120" {
		t.Fatalf("heat IM event mismatch: publish=%t event=%+v", publish, heatEvent)
	}

	rankRecord, err := outbox.Build("room-robot-rank", "RoomRankChanged", 10, time.Now(), &roomeventsv1.RoomRankChanged{
		UserId:    1002,
		Score:     360,
		GiftValue: 360,
	})
	if err != nil {
		t.Fatalf("Build RoomRankChanged envelope failed: %v", err)
	}
	rankEvent, publish, err := roomEventFromEnvelope(rankRecord.Envelope)
	if err != nil {
		t.Fatalf("RoomRankChanged should decode: %v", err)
	}
	if !publish || rankEvent.EventType != "room_rank_changed" || rankEvent.TargetUserID != "1002" || rankEvent.GiftValue != 360 || rankEvent.Attributes["score"] != "360" {
		t.Fatalf("rank IM event mismatch: publish=%t event=%+v", publish, rankEvent)
	}
}

func TestRoomRobotLuckyGiftDrawnPublishesLuckyGiftIMEvent(t *testing.T) {
	record, err := outbox.Build("room-robot-lucky", "RoomRobotLuckyGiftDrawn", 11, time.Now(), &roomeventsv1.RoomRobotLuckyGiftDrawn{
		DrawId:               "robot_lucky_draw_1",
		CommandId:            "cmd_robot_lucky_1",
		SenderUserId:         1001,
		TargetUserId:         1002,
		GiftId:               "gift_lucky_1",
		GiftCount:            1,
		PoolId:               "robot_lucky_display",
		MultiplierPpm:        5000000,
		BaseRewardCoins:      500,
		EffectiveRewardCoins: 500,
		CreatedAtMs:          1778000000000,
		VisibleRegionId:      86,
		IsRobot:              true,
		Synthetic:            true,
	})
	if err != nil {
		t.Fatalf("Build RoomRobotLuckyGiftDrawn envelope failed: %v", err)
	}

	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil {
		t.Fatalf("RoomRobotLuckyGiftDrawn should decode: %v", err)
	}
	if !publish || event.EventType != "lucky_gift_drawn" || event.ActorUserID != "1001" || event.TargetUserID != "1002" || event.GiftValue != 0 {
		t.Fatalf("robot lucky gift IM event mismatch: publish=%t event=%+v", publish, event)
	}
	if event.Attributes["draw_id"] != "robot_lucky_draw_1" ||
		event.Attributes["gift_id"] != "gift_lucky_1" ||
		event.Attributes["pool_id"] != "robot_lucky_display" ||
		event.Attributes["gift_value"] != "0" ||
		event.Attributes["effective_reward_coins"] != "500" ||
		event.Attributes["reward_status"] != "granted" {
		t.Fatalf("robot lucky gift IM attributes mismatch: %+v", event.Attributes)
	}
	payload, err := json.Marshal(event)
	if err != nil {
		t.Fatalf("marshal robot lucky gift IM event failed: %v", err)
	}
	var flattened map[string]any
	if err := json.Unmarshal(payload, &flattened); err != nil {
		t.Fatalf("decode flattened robot lucky gift IM event failed: %v", err)
	}
	if flattened["gift_value"] != "0" || flattened["effective_reward_coins"] != "500" {
		t.Fatalf("robot lucky gift JSON must keep reward display but zero contribution: %s", payload)
	}
}

func TestRoomLuckyGiftDrawnPublishesGrantedOwnerFactAsLuckyGiftIMEvent(t *testing.T) {
	record, err := outbox.Build("room-lucky-owner", "RoomLuckyGiftDrawn", 0, time.Now(), &roomeventsv1.RoomLuckyGiftDrawn{
		DrawId:                    "lucky_draw_1",
		CommandId:                 "cmd_lucky_1",
		PoolId:                    "advanced",
		GiftId:                    "43",
		GiftCount:                 1,
		SenderUserId:              1001,
		TargetUserId:              1002,
		SenderName:                "Lucky Sender",
		SenderAvatar:              "https://cdn.example.test/sender.png",
		SenderDisplayUserId:       "123456",
		SenderPrettyDisplayUserId: "VIP123456",
		VisibleRegionId:           1,
		RoomLocked:                true,
		CountryId:                 86,
		CoinSpent:                 100,
		RuleVersion:               7,
		ExperiencePool:            "normal",
		SelectedTierId:            "50x",
		MultiplierPpm:             50_000_000,
		BaseRewardCoins:           5000,
		EffectiveRewardCoins:      5000,
		HighMultiplier:            true,
		RewardStatus:              "granted",
		WalletTransactionId:       "wallet-lucky-1",
		DrawCreatedAtMs:           1_783_940_398_274,
		RewardGrantedAtMs:         1_783_940_398_300,
	})
	if err != nil {
		t.Fatalf("Build RoomLuckyGiftDrawn envelope failed: %v", err)
	}
	record.EventID = "lucky_gift_drawn:lucky_draw_1"
	record.Envelope.EventId = record.EventID

	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil {
		t.Fatalf("RoomLuckyGiftDrawn should decode: %v", err)
	}
	if !publish || event.EventType != "lucky_gift_drawn" || event.EventID != record.EventID || event.ActorUserID != "1001" || event.TargetUserID != "1002" || event.GiftValue != 5000 {
		t.Fatalf("owner lucky gift IM event mismatch: publish=%t event=%+v", publish, event)
	}
	if event.Attributes["sender_name"] != "Lucky Sender" ||
		event.Attributes["sender_pretty_display_user_id"] != "VIP123456" ||
		event.Attributes["multiplier_ppm"] != "50000000" ||
		event.Attributes["reward_status"] != "granted" ||
		event.Attributes["room_locked"] != "true" ||
		event.Attributes["created_at_ms"] != "1783940398274" ||
		event.Attributes["wallet_transaction_id"] != "wallet-lucky-1" {
		t.Fatalf("owner lucky gift IM attributes mismatch: %+v", event.Attributes)
	}
}

func TestRoomMicChangedCarriesTargetGiftValueInIMAttributes(t *testing.T) {
	record, err := outbox.Build("room-mic-value", "RoomMicChanged", 9, time.Now(), &roomeventsv1.RoomMicChanged{
		ActorUserId:               1001,
		TargetUserId:              1002,
		FromSeat:                  2,
		ToSeat:                    5,
		Action:                    "change",
		TargetGiftValue:           367,
		TargetNickname:            "Mic Alice",
		TargetAvatar:              "https://cdn.example.test/alice.png",
		TargetDisplayUserId:       "200002",
		TargetPrettyDisplayUserId: "VIP200002",
	})
	if err != nil {
		t.Fatalf("Build RoomMicChanged envelope failed: %v", err)
	}

	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil {
		t.Fatalf("RoomMicChanged should decode: %v", err)
	}
	if !publish {
		t.Fatal("RoomMicChanged must publish Tencent IM message")
	}
	if event.EventType != "room_mic_changed" || event.SeatNo != 5 || event.Attributes["target_gift_value"] != "367" {
		t.Fatalf("mic IM event target gift value mismatch: %+v", event)
	}
	if event.Attributes["target_nickname"] != "Mic Alice" ||
		event.Attributes["target_avatar"] != "https://cdn.example.test/alice.png" ||
		event.Attributes["target_display_user_id"] != "200002" ||
		event.Attributes["target_pretty_display_user_id"] != "VIP200002" {
		t.Fatalf("mic IM target display snapshot mismatch: %+v", event.Attributes)
	}
	if _, exists := event.Attributes["nickname"]; exists {
		t.Fatalf("admin mic change must not expose target snapshot as actor nickname: %+v", event.Attributes)
	}
}

func TestRoomMicChangedSelfEventCarriesLegacyDisplayAliases(t *testing.T) {
	record, err := outbox.Build("room-mic-self", "RoomMicChanged", 10, time.Now(), &roomeventsv1.RoomMicChanged{
		ActorUserId:               1002,
		TargetUserId:              1002,
		ToSeat:                    3,
		Action:                    "up",
		TargetNickname:            "Mic Alice",
		TargetAvatar:              "https://cdn.example.test/alice.png",
		TargetDisplayUserId:       "200002",
		TargetPrettyDisplayUserId: "VIP200002",
	})
	if err != nil {
		t.Fatalf("Build RoomMicChanged envelope failed: %v", err)
	}

	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil {
		t.Fatalf("RoomMicChanged should decode: %v", err)
	}
	if !publish {
		t.Fatal("RoomMicChanged must publish Tencent IM message")
	}
	if event.Attributes["nickname"] != "Mic Alice" ||
		event.Attributes["avatar"] != "https://cdn.example.test/alice.png" ||
		event.Attributes["display_user_id"] != "200002" ||
		event.Attributes["pretty_display_user_id"] != "VIP200002" {
		t.Fatalf("self mic IM display aliases mismatch: %+v", event.Attributes)
	}
}

func TestRoomPasswordChangedSkipsRoomGroupIM(t *testing.T) {
	record, err := outbox.Build("room-lock", "RoomPasswordChanged", 8, time.Now(), &roomeventsv1.RoomPasswordChanged{
		ActorUserId:     42,
		Locked:          true,
		VisibleRegionId: 86,
	})
	if err != nil {
		t.Fatalf("Build RoomPasswordChanged envelope failed: %v", err)
	}

	event, publish, err := roomEventFromEnvelope(record.Envelope)
	if err != nil {
		t.Fatalf("RoomPasswordChanged should decode: %v", err)
	}
	if publish {
		t.Fatalf("RoomPasswordChanged must use activity region broadcast, not room group IM: %+v", event)
	}
}

func TestTencentIMPublisherRemovesGroupMemberBeforeKickMessage(t *testing.T) {
	paths := make([]string, 0, 2)
	bodies := make([]map[string]any, 0, 2)
	client, err := tencentim.NewRESTClient(tencentim.RESTConfig{
		SDKAppID:        1400000000,
		SecretKey:       "secret",
		AdminIdentifier: "administrator",
		AdminUserSigTTL: time.Hour,
		HTTPClient: &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			paths = append(paths, request.URL.Path)
			var body map[string]any
			if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
				t.Fatalf("decode request body failed: %v", err)
			}
			bodies = append(bodies, body)
			return &http.Response{
				StatusCode: http.StatusOK,
				Body:       io.NopCloser(bytes.NewReader([]byte(`{"ActionStatus":"OK","ErrorCode":0,"ErrorInfo":""}`))),
				Header:     make(http.Header),
			}, nil
		})},
	})
	if err != nil {
		t.Fatalf("NewRESTClient failed: %v", err)
	}

	record, err := outbox.Build("room-kick", "RoomUserKicked", 3, time.Now(), &roomeventsv1.RoomUserKicked{
		ActorUserId:  1,
		TargetUserId: 2,
	})
	if err != nil {
		t.Fatalf("Build RoomUserKicked envelope failed: %v", err)
	}
	publisher := NewTencentIMPublisher(client)
	if err := publisher.PublishOutboxEvent(context.Background(), record.Envelope); err != nil {
		t.Fatalf("PublishOutboxEvent failed: %v", err)
	}

	if len(paths) != 2 || paths[0] != "/v4/group_open_http_svc/delete_group_member" || paths[1] != "/v4/group_open_http_svc/send_group_msg" {
		t.Fatalf("kick outbox should delete member before system message, paths=%+v", paths)
	}
	if bodies[0]["GroupId"] != "room-kick" || bodies[1]["GroupId"] != "room-kick" {
		t.Fatalf("kick outbox must use original room group id, bodies=%+v", bodies)
	}
}

func TestTencentIMPublisherEnsuresRoomGroupAndRetriesWhenGroupMissing(t *testing.T) {
	paths := make([]string, 0, 3)
	bodies := make([]map[string]any, 0, 3)
	client, err := tencentim.NewRESTClient(tencentim.RESTConfig{
		SDKAppID:        1400000000,
		SecretKey:       "secret",
		AdminIdentifier: "administrator",
		AdminUserSigTTL: time.Hour,
		HTTPClient: &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			paths = append(paths, request.URL.Path)
			var body map[string]any
			if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
				t.Fatalf("decode request body failed: %v", err)
			}
			bodies = append(bodies, body)
			if len(paths) == 1 {
				return &http.Response{
					StatusCode: http.StatusOK,
					Body:       io.NopCloser(bytes.NewReader([]byte(`{"ActionStatus":"FAIL","ErrorCode":10015,"ErrorInfo":"bytes_group_id invalid"}`))),
					Header:     make(http.Header),
				}, nil
			}
			return &http.Response{
				StatusCode: http.StatusOK,
				Body:       io.NopCloser(bytes.NewReader([]byte(`{"ActionStatus":"OK","ErrorCode":0,"ErrorInfo":""}`))),
				Header:     make(http.Header),
			}, nil
		})},
	})
	if err != nil {
		t.Fatalf("NewRESTClient failed: %v", err)
	}

	publisher := NewTencentIMPublisher(client)
	err = publisher.PublishRoomEvent(context.Background(), tencentim.RoomEvent{
		EventID:   "evt-retry",
		RoomID:    "lalu_2963d975-316e-444d-ad5c-f82fc36c28ca",
		EventType: "room_mic_changed",
	})
	if err != nil {
		t.Fatalf("PublishRoomEvent should retry after ensuring group: %v", err)
	}

	wantPaths := []string{
		"/v4/group_open_http_svc/send_group_msg",
		"/v4/group_open_http_svc/create_group",
		"/v4/group_open_http_svc/send_group_msg",
	}
	if len(paths) != len(wantPaths) {
		t.Fatalf("path count mismatch: paths=%+v bodies=%+v", paths, bodies)
	}
	for index, want := range wantPaths {
		if paths[index] != want {
			t.Fatalf("path[%d] mismatch: got %s want %s", index, paths[index], want)
		}
	}
	for index, body := range bodies {
		if body["GroupId"] != "lalu_2963d975-316e-444d-ad5c-f82fc36c28ca" {
			t.Fatalf("request %d must preserve original room group id: %+v", index, body)
		}
	}
}

func TestTencentIMPublisherEnsuresRoomGroupAndRetriesMemberMuteWhenGroupMissing(t *testing.T) {
	paths := make([]string, 0, 3)
	bodies := make([]map[string]any, 0, 3)
	client, err := tencentim.NewRESTClient(tencentim.RESTConfig{
		SDKAppID:        1400000000,
		SecretKey:       "secret",
		AdminIdentifier: "administrator",
		AdminUserSigTTL: time.Hour,
		HTTPClient: &http.Client{Transport: roundTripFunc(func(request *http.Request) (*http.Response, error) {
			paths = append(paths, request.URL.Path)
			var body map[string]any
			if err := json.NewDecoder(request.Body).Decode(&body); err != nil {
				t.Fatalf("decode request body failed: %v", err)
			}
			bodies = append(bodies, body)
			if len(paths) == 1 {
				return &http.Response{
					StatusCode: http.StatusOK,
					Body:       io.NopCloser(bytes.NewReader([]byte(`{"ActionStatus":"FAIL","ErrorCode":10010,"ErrorInfo":"group not exist"}`))),
					Header:     make(http.Header),
				}, nil
			}
			return &http.Response{
				StatusCode: http.StatusOK,
				Body:       io.NopCloser(bytes.NewReader([]byte(`{"ActionStatus":"OK","ErrorCode":0,"ErrorInfo":""}`))),
				Header:     make(http.Header),
			}, nil
		})},
	})
	if err != nil {
		t.Fatalf("NewRESTClient failed: %v", err)
	}

	publisher := NewTencentIMPublisher(client)
	if err := publisher.SetRoomGroupMemberMuted(context.Background(), "room-mute", 42, true); err != nil {
		t.Fatalf("SetRoomGroupMemberMuted should retry after ensuring group: %v", err)
	}

	wantPaths := []string{
		"/v4/group_open_http_svc/forbid_send_msg",
		"/v4/group_open_http_svc/create_group",
		"/v4/group_open_http_svc/forbid_send_msg",
	}
	if len(paths) != len(wantPaths) {
		t.Fatalf("path count mismatch: paths=%+v bodies=%+v", paths, bodies)
	}
	for index, want := range wantPaths {
		if paths[index] != want {
			t.Fatalf("path[%d] mismatch: got %s want %s", index, paths[index], want)
		}
		if bodies[index]["GroupId"] != "room-mute" {
			t.Fatalf("request %d must preserve room group id: %+v", index, bodies[index])
		}
	}
	for _, index := range []int{0, 2} {
		if bodies[index]["MuteTime"] != float64(tencentim.PermanentGroupMuteSeconds) {
			t.Fatalf("mute request %d must use Tencent permanent mute value: %+v", index, bodies[index])
		}
		accounts, ok := bodies[index]["Members_Account"].([]any)
		if !ok || len(accounts) != 1 || accounts[0] != "42" {
			t.Fatalf("mute request %d must target only user 42: %+v", index, bodies[index])
		}
	}
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
	return f(request)
}
