package service

import (
	"sort"
	"strings"
	"testing"
)

func TestRoomListCacheCandidateOrderMatchesPublicHotBuckets(t *testing.T) {
	query := RoomListQuery{Tab: roomListTabHot, ViewerCountryCode: "US"}
	candidates := []roomListCacheCandidate{
		{Entry: roomListCacheTestEntry("other-empty", "AE", 0, 200000, 0), SortValue: 200000},
		{Entry: roomListCacheTestEntry("viewer-online", "US", 1, 100, 0), SortValue: 100},
		{Entry: roomListCachePinnedTestEntry("country-pin", 1, 88, 9000, 2), SortValue: 2},
		{Entry: roomListCacheTestEntry("other-online", "AE", 1, 10000, 0), SortValue: 10000},
		{Entry: roomListCachePinnedTestEntry("region-pin", 0, 99, 10000, 1), SortValue: 1},
		{Entry: roomListCacheTestEntry("viewer-empty", "US", 0, 100000, 0), SortValue: 100000},
	}
	for index := range candidates {
		if !candidates[index].Entry.IsPinned {
			candidates[index].Entry.PinListRank = roomListCacheNormalRank(query, candidates[index].Entry)
		}
	}
	sort.SliceStable(candidates, func(i, j int) bool {
		return roomListCacheCandidateBefore(candidates[i], candidates[j])
	})

	got := make([]string, 0, len(candidates))
	for _, candidate := range candidates {
		got = append(got, candidate.Entry.RoomID)
	}
	want := []string{"region-pin", "country-pin", "viewer-online", "other-online", "viewer-empty", "other-empty"}
	if len(got) != len(want) {
		t.Fatalf("order length mismatch: got=%v want=%v", got, want)
	}
	for index := range want {
		if got[index] != want[index] {
			t.Fatalf("order mismatch: got=%v want=%v", got, want)
		}
	}
}

func TestRoomListCacheCursorContinuesAfterPinnedBoundary(t *testing.T) {
	query := RoomListQuery{
		Tab:                 roomListTabHot,
		CursorRoomID:        "region-pin",
		CursorPinListRank:   0,
		CursorPinWeight:     99,
		CursorPinnedUntilMS: 10000,
		CursorSortScore:     1,
	}
	before := roomListCachePinnedTestEntry("region-pin", 0, 99, 10000, 1)
	after := roomListCachePinnedTestEntry("country-pin", 1, 88, 9000, 2)
	if roomListCacheAfterCursor(query, before, before.SortScore) {
		t.Fatalf("cursor row itself must not be returned again")
	}
	if !roomListCacheAfterCursor(query, after, after.SortScore) {
		t.Fatalf("next pin rank must continue after cursor")
	}
}

func TestRoomListCacheCountryPinOnlyAppliesToSelectedCountry(t *testing.T) {
	query := RoomListQuery{VisibleRegionID: 9001, CountryCode: "AE", ViewerCountryCode: "US", NowMS: 1000}
	pin := RoomListPinEntry{PinType: roomPinTypeCountry, VisibleRegionID: 9001, RoomID: "room-us", Status: "active", PinnedAtMS: 1, ExpiresAtMS: 2000}
	usRoom := roomListCacheTestEntry("room-us", "US", 1, 10, 0)
	aeRoom := roomListCacheTestEntry("room-ae", "AE", 1, 10, 0)
	if roomListCachePinApplies(query, pin, usRoom) {
		t.Fatalf("country pin must not apply to a room outside selected country")
	}
	if !roomListCachePinApplies(query, pin, aeRoom) {
		t.Fatalf("country pin must apply to matching selected country")
	}
}

func TestRoomListCacheAllRegionsUsesDedicatedIndexesWithoutReusingGlobalRegion(t *testing.T) {
	query := RoomListQuery{
		AppCode:           "huwaa",
		VisibleRegionID:   9001,
		AllVisibleRegions: true,
		ViewerCountryCode: "US",
		Tab:               roomListTabHot,
	}
	if !roomListCacheEligible(query) {
		t.Fatal("all-region hot/new homepage queries must be cache eligible")
	}
	legacyPrefix := "room:list_cache:huwaa"
	if got := roomListCachePrefix("huwaa"); got == legacyPrefix || !strings.HasPrefix(got, "room:list_cache:v2:") {
		t.Fatalf("v2 cache must use a namespace isolated from rolling v1 writers: got=%q legacy=%q", got, legacyPrefix)
	}
	if got := roomListCacheReadyKey("huwaa"); strings.HasPrefix(got, legacyPrefix+":") {
		t.Fatalf("v1 ready=1 must not make the v2 cache readable: %q", got)
	}
	keys := roomListCacheNormalKeys(query)
	if len(keys) != 4 {
		t.Fatalf("all-region viewer-country query keys mismatch: %v", keys)
	}
	for _, key := range keys {
		if !strings.Contains(key, ":base:all:hot:") {
			t.Fatalf("all-region query must use dedicated all scope, got %q", key)
		}
		if key == roomListCacheBaseKey("huwaa", roomListTabHot, 0, roomListCacheCountryAll, roomListCacheBucketOnline) {
			t.Fatalf("all-region key must not alias visible_region_id=0 GLOBAL bucket: %q", key)
		}
	}

	entry := roomListCacheTestEntry("other-region", "AE", 1, 100, 0)
	entry.AppCode = "huwaa"
	entry.VisibleRegionID = 9002
	if !roomListCacheCardMatches(query, entry) {
		t.Fatal("all-region query must accept a card from another region")
	}
	refs := roomListCacheEntryRefs("huwaa", entry)
	hasRegionalIndex := false
	hasAllIndex := false
	for _, ref := range refs {
		hasRegionalIndex = hasRegionalIndex || strings.Contains(ref.Key, ":base:hot:9002:")
		hasAllIndex = hasAllIndex || strings.Contains(ref.Key, ":base:all:hot:")
	}
	if !hasRegionalIndex || !hasAllIndex {
		t.Fatalf("each cache card must update regional and all-region indexes: %+v", refs)
	}

	regionPin := RoomListPinEntry{ID: 1, PinType: roomPinTypeRegion, VisibleRegionID: 9002, RoomID: entry.RoomID}
	if !roomListCachePinApplies(query, regionPin, entry) {
		t.Fatal("all-region list must retain the room's own regional pin")
	}
	pinRefs := roomListCachePinRefs("huwaa", regionPin)
	if len(pinRefs) != 2 || pinRefs[0].Key == pinRefs[1].Key || !strings.Contains(pinRefs[1].Key, ":pin:all:region") {
		t.Fatalf("regional pin must update regional and all-region indexes: %+v", pinRefs)
	}
}

func TestRoomListCachePinTypePrecedenceMatchesMySQLCaseOrder(t *testing.T) {
	candidates := make(map[string]roomListCacheCandidate)
	roomARegion := roomListCachePinnedTestEntry("room-a", 0, 100, 10000, 1)
	roomAGlobal := roomListCachePinnedTestEntry("room-a", 0, 10, 10000, 1)
	roomBRegion := roomListCachePinnedTestEntry("room-b", 0, 50, 10000, 1)

	// 先放入高权重 region，再放入低权重 global，确认同房间先选 pin type 而不是 weight。
	roomListCacheKeepBest(candidates, roomListCacheCandidate{Entry: roomARegion, SortValue: roomARegion.SortScore, PinType: roomPinTypeRegion})
	roomListCacheKeepBest(candidates, roomListCacheCandidate{Entry: roomAGlobal, SortValue: roomAGlobal.SortScore, PinType: roomPinTypeGlobal})
	roomListCacheKeepBest(candidates, roomListCacheCandidate{Entry: roomBRegion, SortValue: roomBRegion.SortScore, PinType: roomPinTypeRegion})

	selectedA := candidates["room-a"]
	if selectedA.PinType != roomPinTypeGlobal || selectedA.Entry.PinWeight != 10 {
		t.Fatalf("same-room global pin must suppress higher-weight region pin exactly like MySQL CASE: %+v", selectedA)
	}
	ordered := make([]roomListCacheCandidate, 0, len(candidates))
	for _, candidate := range candidates {
		ordered = append(ordered, candidate)
	}
	sort.SliceStable(ordered, func(i, j int) bool {
		return roomListCacheCandidateBefore(ordered[i], ordered[j])
	})
	if len(ordered) != 2 || ordered[0].Entry.RoomID != "room-b" || ordered[1].Entry.RoomID != "room-a" {
		t.Fatalf("cache order must match MySQL fallback B(region=50), A(global=10): %+v", ordered)
	}
}

func roomListCacheTestEntry(roomID string, country string, online int32, sortScore int64, createdAtMS int64) RoomListEntry {
	return RoomListEntry{
		AppCode:          "lalu",
		RoomID:           roomID,
		VisibleRegionID:  9001,
		OwnerCountryCode: country,
		Status:           "active",
		OnlineCount:      online,
		SortScore:        sortScore,
		CreatedAtMS:      createdAtMS,
	}
}

func roomListCachePinnedTestEntry(roomID string, rank int64, weight int64, expiresAtMS int64, sortScore int64) RoomListEntry {
	entry := roomListCacheTestEntry(roomID, "US", 1, sortScore, 0)
	entry.IsPinned = true
	entry.PinListRank = rank
	entry.PinWeight = weight
	entry.PinnedUntilMS = expiresAtMS
	return entry
}
