package service_test

import (
	"context"
	"encoding/json"
	"testing"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	roomservice "hyapp/services/room-service/internal/room/service"
	"hyapp/services/room-service/internal/testutil/mysqltest"
)

func TestListRoomOnlineUsersReturnsOwnerAdminRoleAndGiftValue(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{
		BillingReceiptId: "receipt-online-users",
		CoinSpent:        321,
		GiftPointAdded:   321,
		HeatValue:        321,
		GiftTypeCode:     "normal",
	}}}
	now := &fixedRoomRocketClock{now: time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC)}
	svc := newRocketTestService(t, repository, wallet, now)

	roomID := "room-online-users"
	ownerID := int64(1001)
	adminID := int64(1002)
	normalID := int64(1003)
	createRocketRoom(t, ctx, svc, roomID, ownerID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, adminID)
	joinRocketRoom(t, ctx, svc, roomID, normalID)

	if _, err := svc.SetRoomAdmin(ctx, &roomv1.SetRoomAdminRequest{
		Meta:         roomservice.NewRequestMeta(roomID, ownerID),
		TargetUserId: adminID,
		Enabled:      true,
	}); err != nil {
		t.Fatalf("set admin failed: %v", err)
	}
	if _, err := svc.SetRoomAdmin(ctx, &roomv1.SetRoomAdminRequest{
		Meta:         roomservice.NewRequestMeta(roomID, ownerID),
		TargetUserId: ownerID,
		Enabled:      true,
	}); err == nil {
		t.Fatalf("setting owner as admin must fail")
	}

	if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta:         rocketMeta(roomID, adminID, "online-users-gift"),
		TargetType:   "user",
		TargetUserId: ownerID,
		GiftId:       "gift-online-users",
		GiftCount:    1,
	}); err != nil {
		t.Fatalf("send gift failed: %v", err)
	}

	resp, err := svc.ListRoomOnlineUsers(ctx, &roomv1.ListRoomOnlineUsersRequest{
		Meta:         &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
		RoomId:       roomID,
		ViewerUserId: ownerID,
		Page:         1,
		PageSize:     10,
		Sort:         "gift_value",
	})
	if err != nil {
		t.Fatalf("list online users failed: %v", err)
	}
	if len(resp.GetItems()) != 3 {
		t.Fatalf("expected three online users, got %+v", resp.GetItems())
	}

	byID := make(map[int64]*roomv1.RoomOnlineUser, len(resp.GetItems()))
	for _, item := range resp.GetItems() {
		byID[item.GetUserId()] = item
	}
	if owner := byID[ownerID]; owner == nil || !owner.GetIsOwner() || owner.GetRoomRole() != "normal" {
		t.Fatalf("owner must be is_owner=true and room_role=normal: %+v", owner)
	}
	if admin := byID[adminID]; admin == nil || admin.GetIsOwner() || admin.GetRoomRole() != "admin" || admin.GetGiftValue() != 321 {
		t.Fatalf("admin gift value or role mismatch: %+v", admin)
	}
	if normal := byID[normalID]; normal == nil || normal.GetIsOwner() || normal.GetRoomRole() != "normal" || normal.GetGiftValue() != 0 {
		t.Fatalf("normal user mismatch: %+v", normal)
	}
	payload, err := json.Marshal(resp.GetItems())
	if err != nil {
		t.Fatalf("marshal online users failed: %v", err)
	}
	t.Logf("online users response items: %s", payload)
}

func TestJoinLeaveRoomUpdatesPresenceIncrementally(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	now := &fixedRoomRocketClock{now: time.Date(2026, 5, 27, 13, 0, 0, 0, time.UTC)}
	svc := newRocketTestService(t, repository, &rocketTestWallet{}, now)

	roomID := "room-presence-incremental"
	ownerID := int64(1101)
	viewerID := int64(1102)
	createRocketRoom(t, ctx, svc, roomID, ownerID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, viewerID)

	onlineResp, err := svc.ListRoomOnlineUsers(ctx, &roomv1.ListRoomOnlineUsersRequest{
		Meta:         &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
		RoomId:       roomID,
		ViewerUserId: ownerID,
		Page:         1,
		PageSize:     10,
	})
	if err != nil {
		t.Fatalf("list online users after join failed: %v", err)
	}
	if item := onlineUserItemByID(onlineResp.GetItems(), viewerID); item == nil || item.GetRole() != "audience" {
		t.Fatalf("join must upsert viewer active presence: %+v", onlineResp.GetItems())
	}

	if _, err := svc.LeaveRoom(ctx, &roomv1.LeaveRoomRequest{Meta: rocketMeta(roomID, viewerID, "presence-incremental-leave")}); err != nil {
		t.Fatalf("leave room failed: %v", err)
	}
	onlineResp, err = svc.ListRoomOnlineUsers(ctx, &roomv1.ListRoomOnlineUsersRequest{
		Meta:         &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
		RoomId:       roomID,
		ViewerUserId: ownerID,
		Page:         1,
		PageSize:     10,
	})
	if err != nil {
		t.Fatalf("list online users after leave failed: %v", err)
	}
	if item := onlineUserItemByID(onlineResp.GetItems(), viewerID); item != nil {
		t.Fatalf("leave must mark only viewer presence left: %+v", onlineResp.GetItems())
	}
}

func TestBatchGetUserVoiceRoomPresencesUsesCurrentMicSeatAndHidesViewer(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	now := &fixedRoomRocketClock{now: time.Date(2026, 7, 17, 8, 0, 0, 0, time.UTC)}
	svc := newRocketTestService(t, repository, &rocketTestWallet{}, now)

	roomID := "room-profile-live"
	ownerID := int64(1301)
	targetID := int64(1302)
	missingID := int64(1303)
	offMicID := int64(1304)
	createRocketRoom(t, ctx, svc, roomID, ownerID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, targetID)
	joinRocketRoom(t, ctx, svc, roomID, offMicID)
	if _, err := svc.MicUp(ctx, &roomv1.MicUpRequest{Meta: rocketMeta(roomID, targetID, "profile-live-mic-up"), SeatNo: 3}); err != nil {
		t.Fatalf("mic up target failed: %v", err)
	}
	if _, err := svc.SetRoomPassword(ctx, &roomv1.SetRoomPasswordRequest{Meta: rocketMeta(roomID, ownerID, "profile-live-lock"), Locked: true, Password: "1234"}); err != nil {
		t.Fatalf("lock room failed: %v", err)
	}

	resp, err := svc.BatchGetUserVoiceRoomPresences(ctx, &roomv1.BatchGetUserVoiceRoomPresencesRequest{
		Meta:         &roomv1.RequestMeta{AppCode: appcode.Default, RequestId: "req-profile-live"},
		ViewerUserId: ownerID,
		UserIds:      []int64{targetID, missingID, offMicID, ownerID, targetID},
	})
	if err != nil {
		t.Fatalf("batch user voice room presence failed: %v", err)
	}
	if len(resp.GetItems()) != 4 {
		t.Fatalf("deduplicated input must return four ordered items: %+v", resp.GetItems())
	}
	target := resp.GetItems()[0]
	if target.GetUserId() != targetID || target.GetState() != "on_mic" || !target.GetJoinable() || target.GetSeatNo() != 3 {
		t.Fatalf("target mic state mismatch: %+v", target)
	}
	if room := target.GetRoom(); room == nil || room.GetRoomId() != roomID || room.GetOwnerUserId() != ownerID || !room.GetLocked() || room.GetSeatCount() != 10 {
		t.Fatalf("target room entry mismatch: %+v", target.GetRoom())
	}
	for _, item := range resp.GetItems()[1:] {
		if item.GetState() != "not_on_mic" || item.GetJoinable() || item.GetRoom() != nil || item.GetSeatNo() != 0 {
			t.Fatalf("missing user and viewer must not expose a room: %+v", item)
		}
	}
	if resp.GetServerTimeMs() != now.now.UnixMilli() || target.GetObservedAtMs() != resp.GetServerTimeMs() {
		t.Fatalf("presence timestamps must use the service UTC clock: %+v", resp)
	}
}

func TestMarkRoomUserPresenceLeftDoesNotClearAnotherRoom(t *testing.T) {
	ctx := appcode.WithContext(context.Background(), appcode.Default)
	repository := mysqltest.NewRepository(t)
	userID := int64(1201)

	if err := repository.UpsertRoomUserPresence(ctx, roomservice.RoomPresence{
		AppCode:      appcode.Default,
		UserID:       userID,
		RoomID:       "room-current-active",
		Role:         "audience",
		RoomVersion:  2,
		JoinedAtMS:   1000,
		LastSeenAtMS: 1000,
		UpdatedAtMS:  1000,
	}); err != nil {
		t.Fatalf("upsert current room presence failed: %v", err)
	}
	if err := repository.MarkRoomUserPresenceLeft(ctx, roomservice.RoomPresence{
		AppCode:     appcode.Default,
		UserID:      userID,
		RoomID:      "room-old-left",
		RoomVersion: 3,
		UpdatedAtMS: 2000,
	}); err != nil {
		t.Fatalf("mark old room presence left failed: %v", err)
	}

	presence, exists, err := repository.GetCurrentRoomPresence(ctx, userID)
	if err != nil {
		t.Fatalf("get current room presence failed: %v", err)
	}
	if !exists || presence.RoomID != "room-current-active" || presence.Status != "active" {
		t.Fatalf("old room leave projection must not clear newer room presence: exists=%v presence=%+v", exists, presence)
	}
}

func onlineUserItemByID(items []*roomv1.RoomOnlineUser, userID int64) *roomv1.RoomOnlineUser {
	for _, item := range items {
		if item.GetUserId() == userID {
			return item
		}
	}
	return nil
}

func TestSendGiftPassesHostPeriodScopeToWallet(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{
		BillingReceiptId:       "receipt-host-period",
		CoinSpent:              14,
		GiftPointAdded:         6,
		HeatValue:              22,
		GiftTypeCode:           "normal",
		HostPeriodDiamondAdded: 14,
		HostPeriodCycleKey:     "2026-05",
	}}}
	now := &fixedRoomRocketClock{now: time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC)}
	svc := newRocketTestService(t, repository, wallet, now)

	roomID := "room-host-period-scope"
	senderID := int64(2001)
	hostID := int64(2002)
	createRocketRoom(t, ctx, svc, roomID, senderID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, hostID)

	if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta:                    rocketMeta(roomID, senderID, "host-period-scope"),
		TargetType:              "user",
		TargetUserId:            hostID,
		GiftId:                  "gift-host-period",
		GiftCount:               2,
		TargetIsHost:            true,
		TargetHostRegionId:      8801,
		TargetAgencyOwnerUserId: 30001,
	}); err != nil {
		t.Fatalf("send host period gift failed: %v", err)
	}

	if wallet.lastDebit == nil || !wallet.lastDebit.GetTargetIsHost() || wallet.lastDebit.GetTargetHostRegionId() != 8801 || wallet.lastDebit.GetTargetAgencyOwnerUserId() != 30001 {
		t.Fatalf("room-service must pass host period scope to wallet: %+v", wallet.lastDebit)
	}
}

func TestSendGiftPassesBagSourceToWallet(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{
		{
			BillingReceiptId: "receipt-bag-single",
			CoinSpent:        20,
			ChargeAmount:     20,
			ChargeAssetType:  "BAG",
			HeatValue:        20,
			GiftTypeCode:     "normal",
		},
		{
			BillingReceiptId: "receipt-bag-batch-1",
			CoinSpent:        10,
			ChargeAmount:     10,
			ChargeAssetType:  "BAG",
			HeatValue:        10,
			GiftTypeCode:     "normal",
		},
		{
			BillingReceiptId: "receipt-bag-batch-2",
			CoinSpent:        10,
			ChargeAmount:     10,
			ChargeAssetType:  "BAG",
			HeatValue:        10,
			GiftTypeCode:     "normal",
		},
	}}
	now := &fixedRoomRocketClock{now: time.Date(2026, 5, 27, 12, 0, 0, 0, time.UTC)}
	svc := newRocketTestService(t, repository, wallet, now)

	roomID := "room-bag-source"
	senderID := int64(2101)
	targetOneID := int64(2102)
	targetTwoID := int64(2103)
	createRocketRoom(t, ctx, svc, roomID, senderID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, targetOneID)
	joinRocketRoom(t, ctx, svc, roomID, targetTwoID)

	if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta:          rocketMeta(roomID, senderID, "bag-single"),
		TargetType:    "user",
		TargetUserId:  targetOneID,
		GiftId:        "gift-bag-single",
		GiftCount:     2,
		EntitlementId: "ent-bag-single",
		Source:        "bag",
	}); err != nil {
		t.Fatalf("send single bag gift failed: %v", err)
	}
	if wallet.lastDebit == nil || wallet.lastDebit.GetEntitlementId() != "ent-bag-single" || wallet.lastDebit.GetChargeSource() != "bag" {
		t.Fatalf("single bag gift must pass entitlement/source to wallet: %+v", wallet.lastDebit)
	}

	if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta:          rocketMeta(roomID, senderID, "bag-batch"),
		TargetType:    "user",
		TargetUserId:  targetOneID,
		TargetUserIds: []int64{targetOneID, targetTwoID},
		GiftId:        "gift-bag-batch",
		GiftCount:     1,
		EntitlementId: "ent-bag-batch",
		Source:        "bag",
	}); err != nil {
		t.Fatalf("send batch bag gift failed: %v", err)
	}
	if wallet.lastBatch == nil || wallet.lastBatch.GetEntitlementId() != "ent-bag-batch" || wallet.lastBatch.GetChargeSource() != "bag" {
		t.Fatalf("batch bag gift must pass entitlement/source to wallet: %+v", wallet.lastBatch)
	}
}
