package service_test

import (
	"context"
	"testing"
	"time"

	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/pkg/appcode"
	"hyapp/pkg/walletmq"
	"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"
)

func TestListRoomsUsesCacheWhenEligible(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	cache := &fakeRoomListCache{entries: []roomservice.RoomListEntry{{
		AppCode:                    appcode.Default,
		RoomID:                     "room-cache-hit",
		RoomShortID:                "cache-hit",
		VisibleRegionID:            7001,
		OwnerUserID:                6001,
		OwnerVIPLevel:              5,
		OwnerVIPExpiresAtMS:        time.Now().UTC().Add(time.Hour).UnixMilli(),
		Title:                      "cache",
		Mode:                       "voice",
		Status:                     "active",
		SortScore:                  99,
		RoomBorderJSON:             `{"resource_id":5201,"resource_type":"room_border","asset_url":"https://cdn.example.com/room-border/gold.webp","expires_at_ms":4102444800000}`,
		RocketNearCompletion:       true,
		HasActiveRedPacket:         true,
		ActiveRedPacketExpiresAtMS: time.Now().UTC().Add(time.Hour).UnixMilli(),
	}}}
	svc := roomservice.New(roomservice.Config{
		NodeID:         "node-list-cache-hit-test",
		LeaseTTL:       10 * time.Second,
		RankLimit:      20,
		SnapshotEveryN: 1,
		RoomListCache:  cache,
	}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())

	resp, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
		Meta:            &roomv1.RequestMeta{AppCode: appcode.Default},
		ViewerUserId:    4001,
		VisibleRegionId: 7001,
		Tab:             "hot",
		Limit:           10,
	})
	if err != nil {
		t.Fatalf("list rooms failed: %v", err)
	}
	if cache.listCalls != 1 {
		t.Fatalf("eligible public list must query cache once, calls=%d", cache.listCalls)
	}
	if got := roomIDs(resp.GetRooms()); len(got) != 1 || got[0] != "room-cache-hit" {
		t.Fatalf("list response must come from cache, got=%v", got)
	}
	if got := resp.GetRooms()[0].GetOwnerVipLevel(); got != 5 {
		t.Fatalf("list response must expose the cached owner VIP without a wallet lookup, got=%d", got)
	}
	if got := resp.GetRooms()[0].GetRoomBorderUrl(); got != "https://cdn.example.com/room-border/gold.webp" || resp.GetRooms()[0].GetRoomBorder().GetAssetUrl() != got {
		t.Fatalf("list response must derive flat room border URL from the existing projection: %+v", resp.GetRooms()[0])
	}
	if !resp.GetRooms()[0].GetRocketNearCompletion() || !resp.GetRooms()[0].GetHasActiveRedPacket() {
		t.Fatalf("list response must expose cached activity indicators without an upstream lookup: %+v", resp.GetRooms()[0])
	}
}

func TestWalletRedPacketEventsProjectAggregateIntoRoomList(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	svc := roomservice.New(roomservice.Config{
		NodeID: "node-red-packet-list-projection-test", LeaseTTL: 10 * time.Second, RankLimit: 20, SnapshotEveryN: 1,
	}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
	createPinnedListRoom(t, ctx, svc, "room-red-packet-list", "red-packet-list", 6301, 7101, "US")
	nowMS := time.Now().UTC().UnixMilli()
	created := walletmq.WalletOutboxMessage{
		AppCode: appcode.Default, EventID: "evt-red-packet-created", EventType: walletmq.EventTypeWalletRedPacketCreated,
		TransactionID: "tx-created", CommandID: "cmd-created", UserID: 6301, AssetType: "COIN", OccurredAtMS: nowMS,
		PayloadJSON: `{"packet_id":"rp-list","room_id":"room-red-packet-list","status":"active","remaining_amount":100,"remaining_count":2,"expires_at_ms":4102444800000}`,
	}
	if err := svc.HandleWalletRedPacketProjectionEvent(ctx, created); err != nil {
		t.Fatalf("project created red packet: %v", err)
	}
	resp, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
		Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: 4001, VisibleRegionId: 7101, Tab: "new", Limit: 10,
	})
	if err != nil || len(resp.GetRooms()) != 1 || !resp.GetRooms()[0].GetHasActiveRedPacket() {
		t.Fatalf("created packet must enable room-list indicator without wallet lookup: err=%v resp=%+v", err, resp)
	}
	secondCreated := walletmq.WalletOutboxMessage{
		AppCode: appcode.Default, EventID: "evt-red-packet-second-created", EventType: walletmq.EventTypeWalletRedPacketCreated,
		TransactionID: "tx-second-created", CommandID: "cmd-second-created", UserID: 6301, AssetType: "COIN", OccurredAtMS: nowMS + 1,
		PayloadJSON: `{"packet_id":"rp-list-second","room_id":"room-red-packet-list","status":"active","remaining_amount":50,"remaining_count":1,"expires_at_ms":4102444700000}`,
	}
	if err := svc.HandleWalletRedPacketProjectionEvent(ctx, secondCreated); err != nil {
		t.Fatalf("project second active red packet: %v", err)
	}

	claimed := walletmq.WalletOutboxMessage{
		AppCode: appcode.Default, EventID: "evt-red-packet-finished", EventType: walletmq.EventTypeWalletRedPacketClaimed,
		TransactionID: "tx-finished", CommandID: "cmd-finished", UserID: 6401, AssetType: "COIN", OccurredAtMS: nowMS + 2,
		PayloadJSON: `{"packet_id":"rp-list","room_id":"room-red-packet-list","packet_status":"finished","remaining_amount":0,"remaining_count":0}`,
	}
	if err := svc.HandleWalletRedPacketProjectionEvent(ctx, claimed); err != nil {
		t.Fatalf("project finished red packet: %v", err)
	}
	resp, err = svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
		Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: 4001, VisibleRegionId: 7101, Tab: "new", Limit: 10,
	})
	if err != nil || len(resp.GetRooms()) != 1 || !resp.GetRooms()[0].GetHasActiveRedPacket() {
		t.Fatalf("finishing one packet must retain indicator while another is active: err=%v resp=%+v", err, resp)
	}

	secondClaimed := walletmq.WalletOutboxMessage{
		AppCode: appcode.Default, EventID: "evt-red-packet-second-finished", EventType: walletmq.EventTypeWalletRedPacketClaimed,
		TransactionID: "tx-second-finished", CommandID: "cmd-second-finished", UserID: 6402, AssetType: "COIN", OccurredAtMS: nowMS + 3,
		PayloadJSON: `{"packet_id":"rp-list-second","room_id":"room-red-packet-list","packet_status":"finished","remaining_amount":0,"remaining_count":0}`,
	}
	if err := svc.HandleWalletRedPacketProjectionEvent(ctx, secondClaimed); err != nil {
		t.Fatalf("project second finished red packet: %v", err)
	}
	resp, err = svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
		Meta: &roomv1.RequestMeta{AppCode: appcode.Default}, ViewerUserId: 4001, VisibleRegionId: 7101, Tab: "new", Limit: 10,
	})
	if err != nil || len(resp.GetRooms()) != 1 || resp.GetRooms()[0].GetHasActiveRedPacket() {
		t.Fatalf("finishing the last packet must clear room-list indicator: err=%v resp=%+v", err, resp)
	}
}

func TestListRoomsFallsBackToMySQLWhenCacheMisses(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	cache := &fakeRoomListCache{ok: false}
	svc := roomservice.New(roomservice.Config{
		NodeID:         "node-list-cache-fallback-test",
		LeaseTTL:       10 * time.Second,
		RankLimit:      20,
		SnapshotEveryN: 1,
		RoomListCache:  cache,
	}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
	createPinnedListRoom(t, ctx, svc, "room-mysql-fallback", "mysql-fallback", 6101, 7101, "US")

	resp, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
		Meta:            &roomv1.RequestMeta{AppCode: appcode.Default},
		ViewerUserId:    4001,
		VisibleRegionId: 7101,
		Tab:             "new",
		Limit:           10,
	})
	if err != nil {
		t.Fatalf("list rooms fallback failed: %v", err)
	}
	if cache.listCalls == 0 {
		t.Fatalf("eligible public list should try cache before MySQL")
	}
	if got := roomIDs(resp.GetRooms()); len(got) != 1 || got[0] != "room-mysql-fallback" {
		t.Fatalf("fallback response must come from MySQL, got=%v", got)
	}
}

func TestListRoomsByOwnersReturnsActiveRoomsForAgencyHosts(t *testing.T) {
	ctx := context.Background()
	repository := mysqltest.NewRepository(t)
	svc := roomservice.New(roomservice.Config{
		NodeID:         "node-list-by-owners-test",
		LeaseTTL:       10 * time.Second,
		RankLimit:      20,
		SnapshotEveryN: 1,
	}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
	createPinnedListRoom(t, ctx, svc, "room-agency-owner", "agency-owner", 6201, 7101, "US")
	createPinnedListRoom(t, ctx, svc, "room-agency-member", "agency-member", 6202, 7202, "AE")
	createPinnedListRoom(t, ctx, svc, "room-outside-agency", "outside-agency", 6203, 7101, "US")
	createPinnedListRoom(t, ctx, svc, "room-agency-closed", "agency-closed", 6202, 7202, "AE")
	if _, err := svc.CloseRoom(ctx, &roomv1.CloseRoomRequest{
		Meta:   roomservice.NewRequestMeta("room-agency-closed", 6202),
		Reason: "closed fixture",
	}); err != nil {
		t.Fatalf("close agency room fixture failed: %v", err)
	}

	resp, err := svc.ListRoomsByOwners(ctx, &roomv1.ListRoomsByOwnersRequest{
		Meta:         &roomv1.RequestMeta{AppCode: appcode.Default},
		ViewerUserId: 4001,
		OwnerUserIds: []int64{6201, 6202},
		Tab:          "new",
		Limit:        10,
	})
	if err != nil {
		t.Fatalf("list rooms by owners failed: %v", err)
	}
	got := roomIDs(resp.GetRooms())
	want := map[string]bool{"room-agency-owner": true, "room-agency-member": true}
	if len(got) != len(want) {
		t.Fatalf("owner-filtered room count mismatch: got=%v", got)
	}
	for _, roomID := range got {
		if !want[roomID] {
			t.Fatalf("owner-filtered rooms must include only active agency host rooms, got=%v", got)
		}
	}
}

type fakeRoomListCache struct {
	entries   []roomservice.RoomListEntry
	ok        bool
	listCalls int
}

func (c *fakeRoomListCache) ListRoomListEntries(context.Context, roomservice.RoomListQuery) ([]roomservice.RoomListEntry, bool, error) {
	c.listCalls++
	if c.ok || c.entries != nil {
		return c.entries, true, nil
	}
	return nil, false, nil
}

func (c *fakeRoomListCache) UpsertRoomListEntry(context.Context, roomservice.RoomListEntry) error {
	return nil
}

func (c *fakeRoomListCache) DeleteRoomListEntry(context.Context, string, string) error {
	return nil
}

func (c *fakeRoomListCache) UpsertRoomPin(context.Context, roomservice.RoomListPinEntry) error {
	return nil
}

func (c *fakeRoomListCache) RebuildRoomList(context.Context, string, []roomservice.RoomListEntry, []roomservice.RoomListPinEntry, int64) error {
	return nil
}
