package service_test

import (
	"context"
	"testing"
	"time"

	"google.golang.org/protobuf/proto"
	luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
	roomv1 "hyapp.local/api/proto/room/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/xerr"
	"hyapp/services/room-service/internal/integration"
	roomservice "hyapp/services/room-service/internal/room/service"
	"hyapp/services/room-service/internal/router"
	"hyapp/services/room-service/internal/testutil/mysqltest"
)

type luckyGiftTestClient struct {
	checks      []*luckygiftv1.CheckLuckyGiftRequest
	draws       []*luckygiftv1.ExecuteLuckyGiftDrawRequest
	batchDraws  []*luckygiftv1.BatchExecuteLuckyGiftDrawRequest
	drawResults []*luckygiftv1.LuckyGiftDrawResult
	// checkStrategy 让 room 测试显式区分 fixed_v2 滚动兼容与 dynamic_v3 强设备事实。
	checkStrategy string
}

func (c *luckyGiftTestClient) CheckLuckyGift(_ context.Context, req *luckygiftv1.CheckLuckyGiftRequest) (*luckygiftv1.CheckLuckyGiftResponse, error) {
	c.checks = append(c.checks, req)
	return &luckygiftv1.CheckLuckyGiftResponse{
		Enabled:         true,
		Reason:          "enabled",
		GiftId:          req.GetGiftId(),
		PoolId:          req.GetPoolId(),
		RuleVersion:     12,
		ExperiencePool:  "novice",
		StrategyVersion: c.checkStrategy,
	}, nil
}

func (c *luckyGiftTestClient) ExecuteLuckyGiftDraw(_ context.Context, req *luckygiftv1.ExecuteLuckyGiftDrawRequest) (*luckygiftv1.ExecuteLuckyGiftDrawResponse, error) {
	c.draws = append(c.draws, req)
	meta := req.GetLuckyGift()
	if len(c.drawResults) > 0 {
		index := len(c.draws) - 1
		if index >= len(c.drawResults) {
			index = len(c.drawResults) - 1
		}
		result := proto.Clone(c.drawResults[index]).(*luckygiftv1.LuckyGiftDrawResult)
		if result.CommandId == "" {
			result.CommandId = meta.GetCommandId()
		}
		if result.PoolId == "" {
			result.PoolId = meta.GetPoolId()
		}
		if result.GiftId == "" {
			result.GiftId = meta.GetGiftId()
		}
		return &luckygiftv1.ExecuteLuckyGiftDrawResponse{Result: result}, nil
	}
	return &luckygiftv1.ExecuteLuckyGiftDrawResponse{Result: &luckygiftv1.LuckyGiftDrawResult{
		DrawId:               "lucky_draw_test",
		CommandId:            meta.GetCommandId(),
		PoolId:               meta.GetPoolId(),
		GiftId:               meta.GetGiftId(),
		RuleVersion:          12,
		ExperiencePool:       "novice",
		SelectedTierId:       "novice_2x",
		MultiplierPpm:        2_000_000,
		BaseRewardCoins:      200,
		EffectiveRewardCoins: 200,
		RewardStatus:         "granted",
		WalletTransactionId:  "wallet_tx_lucky",
		CoinBalanceAfter:     8800,
		CreatedAtMs:          1_779_258_000_000,
	}}, nil
}

func (c *luckyGiftTestClient) BatchExecuteLuckyGiftDraw(_ context.Context, req *luckygiftv1.BatchExecuteLuckyGiftDrawRequest) (*luckygiftv1.BatchExecuteLuckyGiftDrawResponse, error) {
	c.batchDraws = append(c.batchDraws, req)
	resp := &luckygiftv1.BatchExecuteLuckyGiftDrawResponse{Results: make([]*luckygiftv1.LuckyGiftDrawResult, 0, len(req.GetLuckyGifts()))}
	for index, meta := range req.GetLuckyGifts() {
		if len(c.drawResults) > 0 {
			resultIndex := index
			if resultIndex >= len(c.drawResults) {
				resultIndex = len(c.drawResults) - 1
			}
			result := proto.Clone(c.drawResults[resultIndex]).(*luckygiftv1.LuckyGiftDrawResult)
			if result.CommandId == "" {
				result.CommandId = meta.GetCommandId()
			}
			if result.PoolId == "" {
				result.PoolId = meta.GetPoolId()
			}
			if result.GiftId == "" {
				result.GiftId = meta.GetGiftId()
			}
			resp.Results = append(resp.Results, result)
			continue
		}
		resp.Results = append(resp.Results, &luckygiftv1.LuckyGiftDrawResult{
			DrawId:               "lucky_draw_test",
			CommandId:            meta.GetCommandId(),
			PoolId:               meta.GetPoolId(),
			GiftId:               meta.GetGiftId(),
			RuleVersion:          12,
			ExperiencePool:       "novice",
			SelectedTierId:       "novice_2x",
			MultiplierPpm:        2_000_000,
			BaseRewardCoins:      200,
			EffectiveRewardCoins: 200,
			RewardStatus:         "pending",
			CreatedAtMs:          1_779_258_000_000,
		})
	}
	return resp, nil
}

func TestSendGiftReturnsLuckyGiftDrawResult(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{
		{
			BillingReceiptId:       "receipt-lucky",
			CoinSpent:              100,
			GiftPointAdded:         100,
			HeatValue:              100,
			GiftTypeCode:           "super_lucky",
			HostPeriodDiamondAdded: 7,
			// 旧字段故意给出不同的非零值，证明退休后的直接 POINT 不会再进入幸运礼物资金拆分。
			HostPointAdded: 99,
		},
		{
			BillingReceiptId:       "receipt-lucky-legacy-token",
			CoinSpent:              100,
			GiftPointAdded:         100,
			HeatValue:              100,
			GiftTypeCode:           "super_lucky",
			HostPeriodDiamondAdded: 1,
		},
	}}
	luckyGift := &luckyGiftTestClient{}
	svc := roomservice.New(roomservice.Config{
		NodeID:         "node-lucky-test",
		LeaseTTL:       10 * time.Second,
		RankLimit:      20,
		SnapshotEveryN: 1,
	}, router.NewMemoryDirectory(), repository, wallet, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher(), luckyGift)

	roomID := "room-lucky-gift"
	ownerID := int64(101)
	viewerID := int64(202)
	createRocketRoom(t, ctx, svc, roomID, ownerID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, viewerID)

	resp, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta: &roomv1.RequestMeta{
			RequestId:   "req-lucky",
			CommandId:   "cmd-lucky",
			ActorUserId: ownerID,
			RoomId:      roomID,
			SessionId:   "sess-must-not-be-device-1",
			DeviceId:    "device-auth-session-1",
			AppCode:     appcode.Default,
			SentAtMs:    time.Date(2026, 5, 20, 12, 0, 0, 0, time.UTC).UnixMilli(),
		},
		TargetType:   "user",
		TargetUserId: viewerID,
		GiftId:       "rose",
		GiftCount:    1,
		PoolId:       "super_lucky",
		SenderDisplayProfile: &roomv1.SendGiftDisplayProfile{
			UserId:              ownerID,
			Username:            "lucky sender",
			Avatar:              "https://cdn.example.com/sender.png",
			DisplayUserId:       "123456",
			PrettyDisplayUserId: "HY-123456",
		},
	})
	if err != nil {
		t.Fatalf("send lucky gift failed: %v", err)
	}
	if resp.GetLuckyGift().GetDrawId() != "lucky_draw_test" || resp.GetLuckyGift().GetMultiplierPpm() != 2_000_000 || resp.GetLuckyGift().GetEffectiveRewardCoins() != 200 || resp.GetLuckyGift().GetRewardStatus() != "granted" || resp.GetLuckyGift().GetWalletTransactionId() != "wallet_tx_lucky" || resp.GetLuckyGift().GetCoinBalanceAfter() != 0 {
		t.Fatalf("lucky gift result mismatch: %+v", resp.GetLuckyGift())
	}
	if len(resp.GetLuckyGifts()) != 1 || resp.GetLuckyGifts()[0].GetTargetUserId() != viewerID {
		t.Fatalf("single-target lucky_gifts response mismatch: %+v", resp.GetLuckyGifts())
	}
	if len(luckyGift.checks) != 1 || luckyGift.checks[0].GetPoolId() != "super_lucky" || luckyGift.checks[0].GetGiftId() != "rose" {
		t.Fatalf("lucky check request mismatch: %+v", luckyGift.checks)
	}
	if len(luckyGift.draws) != 1 {
		t.Fatalf("expected one lucky draw, got %d", len(luckyGift.draws))
	}
	drawMeta := luckyGift.draws[0].GetLuckyGift()
	if drawMeta.GetCommandId() != "cmd-lucky" ||
		drawMeta.GetCoinSpent() != 100 ||
		drawMeta.GetDeviceId() != "device-auth-session-1" ||
		drawMeta.GetAnchorId() != "101" ||
		drawMeta.GetGiftIncomeCoins() != 7 ||
		drawMeta.GetVisibleRegionId() != 9001 ||
		drawMeta.GetSenderName() != "lucky sender" ||
		drawMeta.GetSenderAvatar() != "https://cdn.example.com/sender.png" ||
		drawMeta.GetSenderDisplayUserId() != "123456" ||
		drawMeta.GetSenderPrettyDisplayUserId() != "HY-123456" {
		t.Fatalf("lucky draw meta mismatch: %+v", drawMeta)
	}

	// 旧 access JWT 没有 device_id 时，room 必须原样传空以保留 fixed_v2 兼容性；
	// 不能因为需要一个非空风控键就用 session_id 或 command_id 代替设备。
	if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta: &roomv1.RequestMeta{
			RequestId: "req-lucky-legacy-token", CommandId: "cmd-lucky-legacy-token",
			ActorUserId: ownerID, RoomId: roomID, SessionId: "sess-legacy-not-device",
			AppCode: appcode.Default, SentAtMs: time.Date(2026, 5, 20, 12, 1, 0, 0, time.UTC).UnixMilli(),
		},
		TargetType: "user", TargetUserId: viewerID, GiftId: "rose", GiftCount: 1, PoolId: "super_lucky",
	}); err != nil {
		t.Fatalf("legacy-token fixed-compatible lucky send failed: %v", err)
	}
	legacyDraw := luckyGift.draws[len(luckyGift.draws)-1].GetLuckyGift()
	if legacyDraw.GetDeviceId() != "" {
		t.Fatalf("legacy-token lucky device_id=%q, want empty instead of session/command fallback", legacyDraw.GetDeviceId())
	}
	if legacyDraw.GetGiftIncomeCoins() != 1 {
		t.Fatalf("legacy host-period income must remain the lucky fund split owner fact: %+v", legacyDraw)
	}

	luckyGift.checkStrategy = "dynamic_v3"
	debitsBeforeDynamicReject := len(wallet.debitRequests)
	_, err = svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta: &roomv1.RequestMeta{
			RequestId: "req-lucky-dynamic-missing-device", CommandId: "cmd-lucky-dynamic-missing-device",
			ActorUserId: ownerID, RoomId: roomID, SessionId: "sess-still-not-a-device",
			AppCode: appcode.Default, SentAtMs: time.Date(2026, 5, 20, 12, 2, 0, 0, time.UTC).UnixMilli(),
		},
		TargetType: "user", TargetUserId: viewerID, GiftId: "rose", GiftCount: 1, PoolId: "super_lucky",
	})
	if !xerr.IsCode(err, xerr.InvalidArgument) {
		t.Fatalf("dynamic_v3 missing signed device_id should fail before debit, got %v", err)
	}
	if len(wallet.debitRequests) != debitsBeforeDynamicReject {
		t.Fatalf("dynamic_v3 missing device reached wallet: before=%d after=%d", debitsBeforeDynamicReject, len(wallet.debitRequests))
	}
}

func TestSendGiftReturnsLuckyGiftDrawResultsForMultipleTargets(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{
		{BillingReceiptId: "receipt-lucky-202", CoinSpent: 100, ChargeAmount: 100, GiftPointAdded: 100, HeatValue: 100, GiftTypeCode: "super_lucky"},
		{BillingReceiptId: "receipt-lucky-303", CoinSpent: 200, ChargeAmount: 200, GiftPointAdded: 200, HeatValue: 200, GiftTypeCode: "super_lucky"},
	}}
	luckyGift := &luckyGiftTestClient{drawResults: []*luckygiftv1.LuckyGiftDrawResult{
		{DrawId: "draw-first", MultiplierPpm: 2_000_000, BaseRewardCoins: 200, EffectiveRewardCoins: 200, RewardStatus: "pending", StageFeedback: true, CreatedAtMs: 1_779_258_000_001},
		{DrawId: "draw-second", MultiplierPpm: 3_000_000, BaseRewardCoins: 300, EffectiveRewardCoins: 300, RewardStatus: "pending", HighMultiplier: true, CreatedAtMs: 1_779_258_000_002},
	}}
	svc := roomservice.New(roomservice.Config{
		NodeID:         "node-lucky-multi-test",
		LeaseTTL:       10 * time.Second,
		RankLimit:      20,
		SnapshotEveryN: 1,
	}, router.NewMemoryDirectory(), repository, wallet, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher(), luckyGift)

	roomID := "room-lucky-gift-multi"
	ownerID := int64(101)
	firstTargetID := int64(202)
	secondTargetID := int64(303)
	createRocketRoom(t, ctx, svc, roomID, ownerID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, firstTargetID)
	joinRocketRoom(t, ctx, svc, roomID, secondTargetID)

	resp, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta: &roomv1.RequestMeta{
			RequestId:   "req-lucky-multi",
			CommandId:   "cmd-lucky-multi",
			ActorUserId: ownerID,
			RoomId:      roomID,
			SessionId:   "sess-must-not-be-device-multi",
			DeviceId:    "device-auth-session-multi",
			AppCode:     appcode.Default,
			SentAtMs:    time.Date(2026, 5, 20, 12, 0, 0, 0, time.UTC).UnixMilli(),
		},
		TargetType:    "user",
		TargetUserIds: []int64{firstTargetID, secondTargetID},
		GiftId:        "rose",
		GiftCount:     1,
		PoolId:        "super_lucky",
	})
	if err != nil {
		t.Fatalf("send multi-target lucky gift failed: %v", err)
	}
	if len(luckyGift.checks) != 1 || luckyGift.checks[0].GetPoolId() != "super_lucky" {
		t.Fatalf("multi-target lucky gift should check pool once: %+v", luckyGift.checks)
	}
	if len(luckyGift.draws) != 0 || len(luckyGift.batchDraws) != 1 {
		t.Fatalf("multi-target lucky gift should call one batch draw: draws=%d batch=%d", len(luckyGift.draws), len(luckyGift.batchDraws))
	}
	if len(luckyGift.batchDraws[0].GetLuckyGifts()) != 2 {
		t.Fatalf("multi-target lucky batch should include two targets: %+v", luckyGift.batchDraws[0])
	}
	firstDraw := luckyGift.batchDraws[0].GetLuckyGifts()[0]
	secondDraw := luckyGift.batchDraws[0].GetLuckyGifts()[1]
	if firstDraw.GetCommandId() != "cmd-lucky-multi:target:202" || firstDraw.GetTargetUserId() != firstTargetID || firstDraw.GetCoinSpent() != 100 {
		t.Fatalf("first target lucky draw mismatch: %+v", firstDraw)
	}
	if secondDraw.GetCommandId() != "cmd-lucky-multi:target:303" || secondDraw.GetTargetUserId() != secondTargetID || secondDraw.GetCoinSpent() != 200 {
		t.Fatalf("second target lucky draw mismatch: %+v", secondDraw)
	}
	if firstDraw.GetDeviceId() != "device-auth-session-multi" || secondDraw.GetDeviceId() != "device-auth-session-multi" {
		t.Fatalf("multi-target draws must share the signed JWT device scope: first=%q second=%q", firstDraw.GetDeviceId(), secondDraw.GetDeviceId())
	}
	if resp.GetLuckyGift().GetCommandId() != "cmd-lucky-multi" ||
		resp.GetLuckyGift().GetTargetUserId() != 0 ||
		resp.GetLuckyGift().GetSelectedTierId() != "batch" ||
		resp.GetLuckyGift().GetMultiplierPpm() != 5_000_000 ||
		resp.GetLuckyGift().GetEffectiveRewardCoins() != 500 ||
		resp.GetLuckyGift().GetWalletTransactionId() != "" ||
		resp.GetLuckyGift().GetCoinBalanceAfter() != 0 ||
		!resp.GetLuckyGift().GetStageFeedback() ||
		!resp.GetLuckyGift().GetHighMultiplier() ||
		len(resp.GetLuckyGifts()) != 2 {
		t.Fatalf("multi-target lucky response must include aggregate and list results: %+v", resp)
	}
	if resp.GetLuckyGifts()[0].GetCommandId() != "cmd-lucky-multi:target:202" || resp.GetLuckyGifts()[0].GetMultiplierPpm() != 2_000_000 || resp.GetLuckyGifts()[1].GetCommandId() != "cmd-lucky-multi:target:303" || resp.GetLuckyGifts()[1].GetMultiplierPpm() != 3_000_000 || resp.GetLuckyGifts()[1].GetTargetUserId() != secondTargetID {
		t.Fatalf("multi-target lucky_gifts response mismatch: %+v", resp.GetLuckyGifts())
	}
}
