package service_test

import (
	"context"
	"testing"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/pkg/appcode"
	"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 recordingRoomGiftLeaderboard struct {
	increments []roomservice.RoomGiftLeaderboardIncrement
}

func (s *recordingRoomGiftLeaderboard) IncrementRoomGift(_ context.Context, input roomservice.RoomGiftLeaderboardIncrement) error {
	s.increments = append(s.increments, input)
	return nil
}

func (s *recordingRoomGiftLeaderboard) ListRoomGiftLeaderboard(context.Context, roomservice.RoomGiftLeaderboardQuery) (roomservice.RoomGiftLeaderboardPage, error) {
	return roomservice.RoomGiftLeaderboardPage{}, nil
}

func TestSendGiftWritesRoomGiftLeaderboardWithHeatValue(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{
		BillingReceiptId: "receipt-gift-leaderboard",
		CoinSpent:        100,
		ChargeAmount:     100,
		GiftPointAdded:   100,
		HeatValue:        10,
		GiftTypeCode:     "lucky",
	}}}
	leaderboard := &recordingRoomGiftLeaderboard{}
	now := &fixedRoomRocketClock{now: time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)}
	svc := roomservice.New(roomservice.Config{
		NodeID:              "node-gift-leaderboard-test",
		LeaseTTL:            10 * time.Second,
		RankLimit:           20,
		SnapshotEveryN:      1,
		Clock:               now,
		RoomGiftLeaderboard: leaderboard,
	}, router.NewMemoryDirectory(), repository, wallet, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())

	roomID := "room-gift-leaderboard"
	senderID := int64(18001)
	targetID := int64(18002)
	createRocketRoom(t, ctx, svc, roomID, senderID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, targetID)

	if _, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta: &roomv1.RequestMeta{
			RequestId:   "req-gift-leaderboard",
			CommandId:   "cmd-gift-leaderboard",
			ActorUserId: senderID,
			RoomId:      roomID,
			AppCode:     appcode.Default,
			SentAtMs:    now.Now().UnixMilli(),
		},
		TargetType:   "user",
		TargetUserId: targetID,
		GiftId:       "gift-leaderboard",
		GiftCount:    1,
	}); err != nil {
		t.Fatalf("send gift failed: %v", err)
	}

	if len(leaderboard.increments) != 1 {
		t.Fatalf("expected one leaderboard increment, got %+v", leaderboard.increments)
	}
	if leaderboard.increments[0].CoinSpent != 10 {
		t.Fatalf("leaderboard must use heat value after global ratio, got %+v", leaderboard.increments[0])
	}

	rankResp, err := svc.ListRoomContributionRank(ctx, &roomv1.ListRoomContributionRankRequest{
		Meta:         &roomv1.RequestMeta{AppCode: appcode.Default},
		RoomId:       roomID,
		Period:       "day",
		Limit:        10,
		ViewerUserId: senderID,
	})
	if err != nil {
		t.Fatalf("list room contribution rank failed: %v", err)
	}
	if len(rankResp.GetItems()) != 1 || rankResp.GetItems()[0].GetUserId() != senderID || rankResp.GetItems()[0].GetGiftValue() != 10 {
		t.Fatalf("room contribution rank must use committed gift heat value: %+v", rankResp.GetItems())
	}
	if rankResp.GetPeriod() != "day" || rankResp.GetStartAtMs() <= 0 || rankResp.GetEndAtMs() <= rankResp.GetStartAtMs() {
		t.Fatalf("room contribution rank period metadata mismatch: %+v", rankResp)
	}
}

func TestAdminRoomContributionUsesCurrentUTCWeek(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{
		{
			BillingReceiptId: "receipt-week-one",
			CoinSpent:        100,
			ChargeAmount:     100,
			HeatValue:        10,
			GiftTypeCode:     "normal",
		},
		{
			BillingReceiptId: "receipt-week-two",
			CoinSpent:        70,
			ChargeAmount:     70,
			HeatValue:        7,
			GiftTypeCode:     "normal",
		},
	}}
	now := &fixedRoomRocketClock{now: time.Date(2026, 6, 5, 12, 0, 0, 0, time.UTC)}
	svc := roomservice.New(roomservice.Config{
		NodeID:         "node-admin-weekly-contribution-test",
		LeaseTTL:       10 * time.Second,
		RankLimit:      20,
		SnapshotEveryN: 1,
		Clock:          now,
	}, router.NewMemoryDirectory(), repository, wallet, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())

	roomID := "room-admin-weekly-contribution"
	senderID := int64(18101)
	targetID := int64(18102)
	createRocketRoom(t, ctx, svc, roomID, senderID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, targetID)
	sendGiftForWeeklyContribution(t, ctx, svc, roomID, senderID, targetID, "cmd-week-one")

	firstWeek, err := svc.AdminListRooms(ctx, &roomv1.AdminListRoomsRequest{
		Meta:          &roomv1.RequestMeta{AppCode: appcode.Default},
		Page:          1,
		PageSize:      20,
		SortBy:        "room_contribution",
		SortDirection: "desc",
	})
	if err != nil {
		t.Fatalf("list first week rooms failed: %v", err)
	}
	if got := adminRoomHeatByID(firstWeek.GetRooms(), roomID); got != 10 {
		t.Fatalf("first week contribution mismatch: got %d want 10", got)
	}

	now.now = time.Date(2026, 6, 8, 0, 0, 0, 0, time.UTC)
	cutoverBuffer, err := svc.AdminListRooms(ctx, &roomv1.AdminListRoomsRequest{
		Meta:          &roomv1.RequestMeta{AppCode: appcode.Default},
		Page:          1,
		PageSize:      20,
		SortBy:        "room_contribution",
		SortDirection: "desc",
	})
	if err != nil {
		t.Fatalf("list cutover buffer rooms failed: %v", err)
	}
	if got := adminRoomHeatByID(cutoverBuffer.GetRooms(), roomID); got != 10 {
		t.Fatalf("monday 00:00 UTC must keep previous contribution until UTC+1 cutover: got %d want 10", got)
	}

	now.now = time.Date(2026, 6, 8, 1, 0, 0, 0, time.UTC)
	nextWeekBeforeGift, err := svc.AdminListRooms(ctx, &roomv1.AdminListRoomsRequest{
		Meta:          &roomv1.RequestMeta{AppCode: appcode.Default},
		Page:          1,
		PageSize:      20,
		SortBy:        "room_contribution",
		SortDirection: "desc",
	})
	if err != nil {
		t.Fatalf("list next contribution week rooms failed: %v", err)
	}
	if got := adminRoomHeatByID(nextWeekBeforeGift.GetRooms(), roomID); got != 0 {
		t.Fatalf("next UTC+1 week must clear contribution before new gifts: got %d want 0", got)
	}

	resp := sendGiftForWeeklyContribution(t, ctx, svc, roomID, senderID, targetID, "cmd-week-two")
	if resp.GetRoomHeat() != 17 {
		t.Fatalf("room heat must remain cumulative Room Cell state: got %d want 17", resp.GetRoomHeat())
	}
	nextWeekAfterGift, err := svc.AdminListRooms(ctx, &roomv1.AdminListRoomsRequest{
		Meta:          &roomv1.RequestMeta{AppCode: appcode.Default},
		Page:          1,
		PageSize:      20,
		SortBy:        "room_contribution",
		SortDirection: "desc",
	})
	if err != nil {
		t.Fatalf("list next week rooms after gift failed: %v", err)
	}
	if got := adminRoomHeatByID(nextWeekAfterGift.GetRooms(), roomID); got != 7 {
		t.Fatalf("next UTC+1 week contribution must restart from new gifts: got %d want 7", got)
	}
}

func TestRoomContributionRankWeekUsesNaturalUTCWeek(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	wallet := &rocketTestWallet{debits: []*walletv1.DebitGiftResponse{{
		BillingReceiptId: "receipt-rank-week-boundary",
		CoinSpent:        100,
		ChargeAmount:     100,
		HeatValue:        10,
		GiftTypeCode:     "normal",
	}}}
	now := &fixedRoomRocketClock{now: time.Date(2026, 6, 8, 0, 30, 0, 0, time.UTC)}
	svc := roomservice.New(roomservice.Config{
		NodeID:         "node-room-rank-week-boundary-test",
		LeaseTTL:       10 * time.Second,
		RankLimit:      20,
		SnapshotEveryN: 1,
		Clock:          now,
	}, router.NewMemoryDirectory(), repository, wallet, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())

	roomID := "room-rank-week-boundary"
	senderID := int64(18201)
	targetID := int64(18202)
	createRocketRoom(t, ctx, svc, roomID, senderID, 9001)
	joinRocketRoom(t, ctx, svc, roomID, targetID)
	sendGiftForWeeklyContribution(t, ctx, svc, roomID, senderID, targetID, "cmd-rank-week-boundary")

	now.now = time.Date(2026, 6, 8, 1, 0, 0, 0, time.UTC)
	rankResp, err := svc.ListRoomContributionRank(ctx, &roomv1.ListRoomContributionRankRequest{
		Meta:         &roomv1.RequestMeta{AppCode: appcode.Default},
		RoomId:       roomID,
		Period:       "week",
		Limit:        10,
		ViewerUserId: senderID,
	})
	if err != nil {
		t.Fatalf("list natural week contribution rank failed: %v", err)
	}
	wantStart := time.Date(2026, 6, 8, 0, 0, 0, 0, time.UTC).UnixMilli()
	if rankResp.GetStartAtMs() != wantStart {
		t.Fatalf("week contribution rank must start at natural UTC Monday 00:00: got %d want %d", rankResp.GetStartAtMs(), wantStart)
	}
	if len(rankResp.GetItems()) != 1 || rankResp.GetItems()[0].GetUserId() != senderID || rankResp.GetItems()[0].GetGiftValue() != 10 {
		t.Fatalf("week contribution rank must keep 00:30 gift in the same natural week after 01:00: %+v", rankResp.GetItems())
	}
}

func sendGiftForWeeklyContribution(t *testing.T, ctx context.Context, svc *roomservice.Service, roomID string, senderID int64, targetID int64, commandID string) *roomv1.SendGiftResponse {
	t.Helper()

	resp, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
		Meta: &roomv1.RequestMeta{
			RequestId:   "req-" + commandID,
			CommandId:   commandID,
			ActorUserId: senderID,
			RoomId:      roomID,
			AppCode:     appcode.Default,
			SentAtMs:    time.Now().UTC().UnixMilli(),
		},
		TargetType:   "user",
		TargetUserId: targetID,
		GiftId:       "gift-weekly-contribution",
		GiftCount:    1,
	})
	if err != nil {
		t.Fatalf("send gift %s failed: %v", commandID, err)
	}
	return resp
}

func adminRoomHeatByID(items []*roomv1.AdminRoomListItem, roomID string) int64 {
	for _, item := range items {
		if item.GetRoomId() == roomID {
			return item.GetHeat()
		}
	}
	return -1
}
