package activityapi

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/pkg/userleaderboard"
	"hyapp/services/gateway-service/internal/appconfig"
	"hyapp/services/gateway-service/internal/auth"

	roomv1 "hyapp.local/api/proto/room/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
)

type fakeUserLeaderboardPresentationReader struct {
	last     *walletv1.BatchGetUserEquippedResourcesRequest
	response *walletv1.BatchGetUserEquippedResourcesResponse
}

func (*fakeUserLeaderboardPresentationReader) GetResource(context.Context, *walletv1.GetResourceRequest) (*walletv1.GetResourceResponse, error) {
	return &walletv1.GetResourceResponse{}, nil
}

func (*fakeUserLeaderboardPresentationReader) GetResourceGroup(context.Context, *walletv1.GetResourceGroupRequest) (*walletv1.GetResourceGroupResponse, error) {
	return &walletv1.GetResourceGroupResponse{}, nil
}

func (*fakeUserLeaderboardPresentationReader) ListGiftConfigs(context.Context, *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error) {
	return &walletv1.ListGiftConfigsResponse{}, nil
}

func (*fakeUserLeaderboardPresentationReader) DebitWheelDraw(context.Context, *walletv1.DebitWheelDrawRequest) (*walletv1.DebitWheelDrawResponse, error) {
	return &walletv1.DebitWheelDrawResponse{}, nil
}

func (f *fakeUserLeaderboardPresentationReader) BatchGetUserEquippedResources(_ context.Context, request *walletv1.BatchGetUserEquippedResourcesRequest) (*walletv1.BatchGetUserEquippedResourcesResponse, error) {
	f.last = request
	if f.response != nil {
		return f.response, nil
	}
	return &walletv1.BatchGetUserEquippedResourcesResponse{}, nil
}

func TestUserLeaderboardPeriodWindowUsesUTCBoundaries(t *testing.T) {
	now := time.Date(2026, 5, 20, 15, 30, 0, 0, time.FixedZone("CST", 8*60*60))

	todayStart, todayEnd := userLeaderboardPeriodWindow(leaderboardPeriodToday, now)
	if todayStart != time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC) {
		t.Fatalf("today start = %s", todayStart)
	}
	if !todayEnd.Equal(now.UTC()) {
		t.Fatalf("today end = %s", todayEnd)
	}

	weekStart, _ := userLeaderboardPeriodWindow(leaderboardPeriodWeek, now)
	if weekStart != time.Date(2026, 5, 18, 0, 0, 0, 0, time.UTC) {
		t.Fatalf("week start = %s", weekStart)
	}

	monthStart, _ := userLeaderboardPeriodWindow(leaderboardPeriodMonth, now)
	if monthStart != time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC) {
		t.Fatalf("month start = %s", monthStart)
	}
}

func TestNormalizeUserLeaderboardTypeAndPeriod(t *testing.T) {
	if normalizeUserLeaderboardType("receiver") != leaderboardTypeReceived {
		t.Fatal("receiver should normalize to received")
	}
	if normalizeUserLeaderboardType("rooms") != leaderboardTypeRoom {
		t.Fatal("rooms should normalize to room")
	}
	if normalizeUserLeaderboardType("gaming") != leaderboardTypeGame {
		t.Fatal("gaming should normalize to game")
	}
	if normalizeUserLeaderboardPeriod("monthly") != leaderboardPeriodMonth {
		t.Fatal("monthly should normalize to month")
	}
}

func TestUserLeaderboardShortBadgeCoversFromResponse(t *testing.T) {
	response := &walletv1.BatchGetUserEquippedResourcesResponse{Users: []*walletv1.UserEquippedResources{{
		UserId:                 20002,
		AllowedVipBenefitCodes: []string{"leaderboard_invisible"},
		Resources: []*walletv1.UserResourceEntitlement{
			{Resource: &walletv1.Resource{ResourceId: 1, ResourceType: "badge", PreviewUrl: "https://cdn.example/short.png", MetadataJson: `{"badge_form":"tile","badge_kind":"normal"}`}},
			{Resource: &walletv1.Resource{ResourceId: 2, ResourceType: "badge", PreviewUrl: "https://cdn.example/long.png", MetadataJson: `{"badge_form":"strip","badge_kind":"normal"}`}},
			{Resource: &walletv1.Resource{ResourceId: 3, ResourceType: "badge", PreviewUrl: "https://cdn.example/level.png", MetadataJson: `{"badge_form":"icon","badge_kind":"level"}`}},
			{Resource: &walletv1.Resource{ResourceId: 4, ResourceType: "badge", AssetUrl: "https://cdn.example/animated.svga", MetadataJson: `{"badge_form":"short","badge_kind":"normal"}`}},
			{Resource: &walletv1.Resource{ResourceId: 5, ResourceType: "badge", PreviewUrl: "https://cdn.example/short.png", MetadataJson: `{"badge_form":"medal","badge_kind":"normal"}`}},
		},
	}}}

	covers := userLeaderboardShortBadgeCoversFromResponse(response)
	if len(covers[20002]) != 1 || covers[20002][0] != "https://cdn.example/short.png" {
		t.Fatalf("only unique normal short-badge covers should be returned: %+v", covers)
	}
	if len(covers[99999]) != 0 {
		t.Fatalf("users without a short badge must stay absent: %+v", covers)
	}
	presentation := userLeaderboardPresentationFromResponse(response)
	if _, invisible := presentation.LeaderboardInvisible[20002]; !invisible {
		t.Fatalf("wallet's final leaderboard privacy grant must be preserved: %+v", presentation)
	}
	item := userLeaderboardItem{UserID: "20002", User: &userLeaderboardUser{UserID: "20002", DisplayUserID: "251145", Username: "VIP"}}
	applyUserLeaderboardPrivacy(&item, presentation.LeaderboardInvisible)
	if !item.IdentityHidden || item.UserID != "" || item.User != nil {
		t.Fatalf("invisible leaderboard identity must be removed without changing rank facts: %+v", item)
	}
}

func TestRoomLeaderboardRoomFromSnapshotIncludesAvatar(t *testing.T) {
	room := roomLeaderboardRoomFromSnapshot("room_1", &roomv1.RoomSnapshot{
		RoomId:      "room_1",
		RoomShortId: "1001",
		RoomExt: map[string]string{
			"title":     "Music Room",
			"cover_url": "https://cdn.example/room.png",
		},
	})

	if room.RoomID != "room_1" || room.RoomShortID != "1001" || room.Title != "Music Room" {
		t.Fatalf("room profile mismatch: %+v", room)
	}
	if room.CoverURL != "https://cdn.example/room.png" || room.RoomAvatar != room.CoverURL {
		t.Fatalf("room avatar fields mismatch: %+v", room)
	}
}

func TestUserLeaderboardItemsFromRoomGiftLeaderboardUsesRoomCard(t *testing.T) {
	items := userLeaderboardItemsFromRoomGiftLeaderboard([]*roomv1.RoomGiftLeaderboardItem{{
		Rank:      1,
		RoomId:    "room_1",
		CoinSpent: 900,
		Room: &roomv1.RoomListItem{
			RoomId:      "room_1",
			RoomShortId: "1001",
			Title:       "Music Room",
			CoverUrl:    "https://cdn.example/room.png",
		},
	}})

	if len(items) != 1 || items[0].RoomID != "room_1" || items[0].GiftValue != 900 {
		t.Fatalf("room leaderboard item mismatch: %+v", items)
	}
	if items[0].Room == nil || items[0].Room.RoomShortID != "1001" || items[0].Room.RoomAvatar != "https://cdn.example/room.png" {
		t.Fatalf("room leaderboard card mismatch: %+v", items[0].Room)
	}
}

func TestListUserLeaderboardsReadsRedisStore(t *testing.T) {
	store := &fakeUserLeaderboardStore{
		page: userleaderboard.Page{
			Items: []userleaderboard.Entry{{
				Rank:             1,
				UserID:           "20002",
				GiftValue:        900,
				GiftCount:        3,
				TransactionCount: 2,
				LastGiftAtMS:     1782461920000,
			}},
			Total:        1,
			BoardType:    leaderboardTypeSent,
			Period:       leaderboardPeriodWeek,
			StartAtMS:    1782172800000,
			EndAtMS:      1782461920000,
			ServerTimeMS: 1782461920000,
		},
		rank: userleaderboard.Entry{
			Rank:             9,
			UserID:           "10001",
			GiftValue:        300,
			GiftCount:        1,
			TransactionCount: 1,
			LastGiftAtMS:     1782460000000,
		},
		rankFound: true,
	}
	capabilityReader := &fakeRuntimeCapabilityReader{capabilities: appconfig.RuntimeCapabilities{
		ActivityLeaderboardScope:     appconfig.ActivityLeaderboardScopeViewerRegion,
		ResourceEquipCommandIDPolicy: appconfig.ResourceEquipCommandIDPolicyLegacyOptional,
	}}
	walletReader := &fakeUserLeaderboardPresentationReader{response: &walletv1.BatchGetUserEquippedResourcesResponse{Users: []*walletv1.UserEquippedResources{
		{UserId: 20002, AllowedVipBenefitCodes: []string{"leaderboard_invisible"}},
		{UserId: 10001},
	}}}
	handler := &Handler{
		userLeaderboard:     store,
		runtimeCapabilities: capabilityReader,
		walletClient:        walletReader,
		userProfileClient: &fakeInviteActivityUserProfileClient{user: &userv1.User{
			AppCode: "hyapp_room", RegionId: 1001,
		}},
	}
	request := httptest.NewRequest(http.MethodGet, "/api/v1/activities/user-leaderboards?board_type=sent&period=week&page=1&page_size=20", nil)
	ctx := appcode.WithContext(request.Context(), "hyapp_room")
	ctx = auth.WithUserID(ctx, 10001)
	recorder := httptest.NewRecorder()

	handler.listUserLeaderboards(recorder, request.WithContext(ctx))

	if recorder.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", recorder.Code, recorder.Body.String())
	}
	if store.listCalls != 1 || store.rankCalls != 1 {
		t.Fatalf("store call count mismatch: list=%d rank=%d", store.listCalls, store.rankCalls)
	}
	if capabilityReader.calls != 1 || store.listQuery.RegionID != 1001 || store.listQuery.AppCode != "hyapp_room" || store.listQuery.BoardType != leaderboardTypeSent || store.listQuery.Period != leaderboardPeriodWeek || store.listQuery.Page != 1 || store.listQuery.PageSize != 20 {
		t.Fatalf("list query mismatch: %+v", store.listQuery)
	}

	var envelope struct {
		Code string                  `json:"code"`
		Data userLeaderboardResponse `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	if envelope.Code != "OK" || envelope.Data.Total != 1 || len(envelope.Data.Items) != 1 {
		t.Fatalf("response mismatch: %+v", envelope)
	}
	if !envelope.Data.Items[0].IdentityHidden || envelope.Data.Items[0].UserID != "" || envelope.Data.Items[0].User != nil || envelope.Data.Items[0].GiftValue != 900 || envelope.Data.MyRank == nil || envelope.Data.MyRank.Rank != 9 {
		t.Fatalf("leaderboard data mismatch: %+v my=%+v", envelope.Data.Items, envelope.Data.MyRank)
	}
	if len(walletReader.last.GetVipBenefitCodes()) != 1 || walletReader.last.GetVipBenefitCodes()[0] != "leaderboard_invisible" {
		t.Fatalf("leaderboard privacy must use the wallet batch gate: %+v", walletReader.last)
	}
}

func TestListUserLeaderboardsAcceptsGameBoard(t *testing.T) {
	store := &fakeUserLeaderboardStore{
		page: userleaderboard.Page{
			Items: []userleaderboard.Entry{{
				Rank:             1,
				UserID:           "20002",
				GiftValue:        1200,
				TransactionCount: 4,
				LastGiftAtMS:     1784095200000,
			}},
			Total:        1,
			BoardType:    leaderboardTypeGame,
			Period:       leaderboardPeriodToday,
			StartAtMS:    1784073600000,
			EndAtMS:      1784095200000,
			ServerTimeMS: 1784095200000,
		},
		rank: userleaderboard.Entry{
			Rank:             8,
			UserID:           "10001",
			GiftValue:        180,
			TransactionCount: 2,
			LastGiftAtMS:     1784095000000,
		},
		rankFound: true,
	}
	profileReader := &fakeInviteActivityUserProfileClient{user: &userv1.User{
		AppCode: "huwaa", RegionId: 1001,
	}}
	handler := &Handler{
		userLeaderboard: store,
		walletClient:    &fakeUserLeaderboardPresentationReader{},
		runtimeCapabilities: &fakeRuntimeCapabilityReader{capabilities: appconfig.RuntimeCapabilities{
			ActivityLeaderboardScope:     appconfig.ActivityLeaderboardScopeAppGlobal,
			ResourceEquipCommandIDPolicy: appconfig.ResourceEquipCommandIDPolicyLegacyOptional,
		}},
		userProfileClient: profileReader,
	}
	request := httptest.NewRequest(http.MethodGet, "/api/v1/activities/user-leaderboards?board_type=gaming&period=today&page=1&page_size=20", nil)
	ctx := appcode.WithContext(request.Context(), "huwaa")
	ctx = auth.WithUserID(ctx, 10001)
	recorder := httptest.NewRecorder()

	handler.listUserLeaderboards(recorder, request.WithContext(ctx))

	if recorder.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", recorder.Code, recorder.Body.String())
	}
	if profileReader.getCalls != 0 || store.listCalls != 1 || store.rankCalls != 1 || store.listQuery.RegionID != 0 || store.listQuery.AppCode != "huwaa" || store.listQuery.BoardType != leaderboardTypeGame || store.rankQuery.BoardType != leaderboardTypeGame {
		t.Fatalf("game query should use its own Redis bucket: list=%+v rank=%+v", store.listQuery, store.rankQuery)
	}
	var envelope struct {
		Code string                  `json:"code"`
		Data userLeaderboardResponse `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	if envelope.Code != "OK" || envelope.Data.BoardType != leaderboardTypeGame || len(envelope.Data.Items) != 1 || envelope.Data.Items[0].GiftValue != 1200 || envelope.Data.MyRank == nil || envelope.Data.MyRank.Rank != 8 {
		t.Fatalf("game leaderboard response mismatch: %+v", envelope.Data)
	}
}

type fakeRuntimeCapabilityReader struct {
	capabilities appconfig.RuntimeCapabilities
	err          error
	calls        int
}

func (f *fakeRuntimeCapabilityReader) GetRuntimeCapabilities(context.Context, string) (appconfig.RuntimeCapabilities, error) {
	f.calls++
	return f.capabilities, f.err
}

type fakeUserLeaderboardStore struct {
	listCalls int
	rankCalls int
	listQuery userleaderboard.Query
	rankQuery userleaderboard.Query
	page      userleaderboard.Page
	rank      userleaderboard.Entry
	rankFound bool
}

func (f *fakeUserLeaderboardStore) List(_ context.Context, query userleaderboard.Query) (userleaderboard.Page, error) {
	f.listCalls++
	f.listQuery = query
	return f.page, nil
}

func (f *fakeUserLeaderboardStore) RankForUser(_ context.Context, query userleaderboard.Query, userID int64) (userleaderboard.Entry, bool, error) {
	f.rankCalls++
	f.rankQuery = query
	if userID != 10001 {
		return userleaderboard.Entry{}, false, nil
	}
	return f.rank, f.rankFound, nil
}

type fakeUserLeaderboardRoomQueryClient struct {
	snapshotResp *roomv1.GetRoomSnapshotResponse
	snapshotErr  error
}

func (f *fakeUserLeaderboardRoomQueryClient) ListRooms(context.Context, *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) {
	return &roomv1.ListRoomsResponse{}, nil
}

func (f *fakeUserLeaderboardRoomQueryClient) ListRoomsByOwners(context.Context, *roomv1.ListRoomsByOwnersRequest) (*roomv1.ListRoomsResponse, error) {
	return &roomv1.ListRoomsResponse{}, nil
}

func (f *fakeUserLeaderboardRoomQueryClient) ListRoomFeeds(context.Context, *roomv1.ListRoomFeedsRequest) (*roomv1.ListRoomsResponse, error) {
	return &roomv1.ListRoomsResponse{}, nil
}

func (f *fakeUserLeaderboardRoomQueryClient) ListRoomGiftLeaderboard(context.Context, *roomv1.ListRoomGiftLeaderboardRequest) (*roomv1.ListRoomGiftLeaderboardResponse, error) {
	return &roomv1.ListRoomGiftLeaderboardResponse{}, nil
}

func (f *fakeUserLeaderboardRoomQueryClient) ListRoomContributionRank(context.Context, *roomv1.ListRoomContributionRankRequest) (*roomv1.ListRoomContributionRankResponse, error) {
	return &roomv1.ListRoomContributionRankResponse{}, nil
}

func (f *fakeUserLeaderboardRoomQueryClient) GetMyRoom(context.Context, *roomv1.GetMyRoomRequest) (*roomv1.GetMyRoomResponse, error) {
	return &roomv1.GetMyRoomResponse{}, nil
}

func (f *fakeUserLeaderboardRoomQueryClient) GetCurrentRoom(context.Context, *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) {
	return &roomv1.GetCurrentRoomResponse{}, nil
}

func (f *fakeUserLeaderboardRoomQueryClient) GetRoomSnapshot(context.Context, *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) {
	if f.snapshotErr != nil {
		return nil, f.snapshotErr
	}
	if f.snapshotResp != nil {
		return f.snapshotResp, nil
	}
	return &roomv1.GetRoomSnapshotResponse{}, nil
}

func (f *fakeUserLeaderboardRoomQueryClient) GetRoomRocket(context.Context, *roomv1.GetRoomRocketRequest) (*roomv1.GetRoomRocketResponse, error) {
	return &roomv1.GetRoomRocketResponse{}, nil
}

func (f *fakeUserLeaderboardRoomQueryClient) ListRoomBackgrounds(context.Context, *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error) {
	return &roomv1.ListRoomBackgroundsResponse{}, nil
}

func (f *fakeUserLeaderboardRoomQueryClient) ListRoomOnlineUsers(context.Context, *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error) {
	return &roomv1.ListRoomOnlineUsersResponse{}, nil
}

func (f *fakeUserLeaderboardRoomQueryClient) ListRoomBannedUsers(context.Context, *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
	return &roomv1.ListRoomBannedUsersResponse{}, nil
}
