package roomapi

import (
	"bytes"
	"context"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

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

func TestGiftHostAuthorityDependencyFailsClosed(t *testing.T) {
	handler := New(Config{})
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", nil)

	if _, _, _, err := handler.resolveGiftTargetHostScope(request, 42); status.Code(err) != codes.Unavailable {
		t.Fatalf("single gift missing Host authority must be unavailable: %v", err)
	}
	if _, err := handler.resolveGiftTargetHostScopes(request, []int64{42, 43}); status.Code(err) != codes.Unavailable {
		t.Fatalf("batch gift missing Host authority must be unavailable: %v", err)
	}
}

func TestCreateRoomRejectsMissingRoomAvatar(t *testing.T) {
	handler := New(Config{})
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/create", bytes.NewBufferString(`{
		"command_id": "cmd-create-room",
		"seat_count": 10,
		"mode": "voice",
		"room_name": "No Cover Room"
	}`))
	recorder := httptest.NewRecorder()

	handler.createRoom(recorder, request)

	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("missing room_avatar must fail before upstream calls: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if !bytes.Contains(recorder.Body.Bytes(), []byte(`"code":"INVALID_ARGUMENT"`)) {
		t.Fatalf("missing room_avatar must return invalid argument envelope: %s", recorder.Body.String())
	}
}

func TestGetRoomShareReturnsShareLinks(t *testing.T) {
	queryClient := &shareRoomQueryClient{snapshotResp: &roomv1.GetRoomSnapshotResponse{
		Room: &roomv1.RoomSnapshot{
			RoomId:      "lalu_room_1",
			OwnerUserId: 10001,
			RoomShortId: "888888",
			RoomExt: map[string]string{
				"title":       "Lalu Room",
				"description": "Join the party",
				"cover_url":   "https://cdn.example.test/room.png",
			},
		},
	}}
	handler := New(Config{RoomQueryClient: queryClient})
	request := httptest.NewRequest(http.MethodGet, "https://api.example.test/api/v1/rooms/lalu_room_1/share", nil)
	request.SetPathValue("room_id", "lalu_room_1")
	request.Header.Set("X-Forwarded-Proto", "https")
	request.Header.Set("X-Forwarded-Host", "share.example.test")
	recorder := httptest.NewRecorder()

	handler.getRoomShare(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("share endpoint status mismatch: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if queryClient.lastSnapshot == nil || queryClient.lastSnapshot.GetRoomId() != "lalu_room_1" {
		t.Fatalf("share endpoint must fetch room snapshot: %+v", queryClient.lastSnapshot)
	}
	body := recorder.Body.Bytes()
	for _, want := range [][]byte{
		[]byte(`"title":"Lalu Room"`),
		[]byte(`"landing_url":"https://share.example.test/share/rooms/lalu_room_1?source=room_share"`),
		[]byte(`"deep_link":"laluparty://room/lalu_room_1?source=room_share"`),
		[]byte(`"image_url":"https://cdn.example.test/room.png"`),
	} {
		if !bytes.Contains(body, want) {
			t.Fatalf("share response missing %s: %s", want, recorder.Body.String())
		}
	}
}

func TestGetRoomShareLandingReturnsOGMeta(t *testing.T) {
	handler := New(Config{RoomQueryClient: &shareRoomQueryClient{snapshotResp: &roomv1.GetRoomSnapshotResponse{
		Room: &roomv1.RoomSnapshot{
			RoomId:      "lalu_room_1",
			OwnerUserId: 10001,
			RoomExt: map[string]string{
				"title":       "Lalu Room",
				"description": "Join the party",
				"cover_url":   "https://cdn.example.test/room.png",
			},
		},
	}}})
	request := httptest.NewRequest(http.MethodGet, "https://share.example.test/share/rooms/lalu_room_1?invite_code=LZSWI6GO", nil)
	request.SetPathValue("room_id", "lalu_room_1")
	recorder := httptest.NewRecorder()

	handler.getRoomShareLanding(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("landing status mismatch: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	body := recorder.Body.String()
	for _, want := range []string{
		`<meta property="og:title" content="Lalu Room">`,
		`<meta property="og:image" content="https://cdn.example.test/room.png">`,
		`<meta name="twitter:card" content="summary_large_image">`,
		`<meta property="al:android:package" content="com.org.laluparty">`,
		`laluparty://room/lalu_room_1?invite_code=LZSWI6GO&amp;source=room_share`,
	} {
		if !bytes.Contains([]byte(body), []byte(want)) {
			t.Fatalf("landing html missing %q: %s", want, body)
		}
	}
}

func TestRoomGiftTabsUsesTypeCodeAsKeyAndTabKeyAsLabel(t *testing.T) {
	tabs := roomGiftTabs(
		[]giftConfigData{
			{GiftTypeCode: "normal"},
			{GiftTypeCode: "lucky"},
			{GiftTypeCode: "disabled"},
		},
		[]giftTypeConfigData{
			{TypeCode: "normal", Name: "普通礼物", TabKey: "Gift", SortOrder: 10},
			{TypeCode: "lucky", Name: "幸运礼物", TabKey: "Lucky", SortOrder: 20},
			{TypeCode: "empty", Name: "空礼物", TabKey: "Empty", SortOrder: 30},
		},
	)

	if len(tabs) != 3 {
		t.Fatalf("tab count mismatch: got %d tabs=%+v", len(tabs), tabs)
	}
	if tabs[1].Key != "normal" || tabs[1].GiftTypeCode != "normal" || tabs[1].Label != "Gift" || tabs[1].Order != 10 {
		t.Fatalf("normal tab mismatch: %+v", tabs[1])
	}
	if tabs[2].Key != "lucky" || tabs[2].GiftTypeCode != "lucky" || tabs[2].Label != "Lucky" || tabs[2].Order != 20 {
		t.Fatalf("lucky tab mismatch: %+v", tabs[2])
	}
}

func TestRoomGiftTabsIncludesBagForOwnedGifts(t *testing.T) {
	tabs := roomGiftTabs(
		[]giftConfigData{
			{GiftTypeCode: "normal"},
			{GiftTypeCode: "bag", Source: "bag", EntitlementID: "ent-1"},
		},
		[]giftTypeConfigData{
			{TypeCode: "normal", Name: "普通礼物", TabKey: "Gift", SortOrder: 10},
		},
	)

	if len(tabs) != 3 {
		t.Fatalf("tab count mismatch: got %d tabs=%+v", len(tabs), tabs)
	}
	if tabs[1].Key != "normal" || tabs[1].Label != "Gift" {
		t.Fatalf("normal tab mismatch: %+v", tabs[1])
	}
	if tabs[2].Key != "bag" || tabs[2].GiftTypeCode != "bag" || tabs[2].Label != "Bag" || tabs[2].Order != 5 {
		t.Fatalf("bag tab mismatch: %+v", tabs[2])
	}
}

func TestRoomGiftBagPanelUsesPinnedCatalogAfterLiveGiftEditOrDisable(t *testing.T) {
	pinned := &walletv1.GiftConfig{
		GiftId: "season-star", ResourceId: 501, Status: "active", Name: "Season Star V1", PriceVersion: "v1",
		CoinPrice: 7, GiftTypeCode: "cp", CpRelationType: "brother", RegionIds: []int64{1},
		PresentationJson: `{"theme":"season-one"}`,
		Resource: &walletv1.Resource{ResourceId: 501, ResourceType: "gift", Status: "active", Name: "Season Star V1",
			PreviewUrl: "https://static.example.test/season-star-v1.png", AnimationUrl: "https://static.example.test/season-star-v1.svga"},
	}
	resources := &walletv1.ListUserResourcesResponse{Resources: []*walletv1.UserResourceEntitlement{{
		EntitlementId: "ent-pinned-star", ResourceId: 501, Quantity: 3, RemainingQuantity: 2,
		SourceSnapshotId: "rgs-season-one", PinnedGiftConfigs: []*walletv1.GiftConfig{pinned},
	}}}
	liveV2 := giftConfigData{
		GiftID: "season-star", ResourceID: 501, Name: "Mutable Star V2", PriceVersion: "v2", CoinPrice: 31,
		Resource: resourceData{ResourceID: 501, ResourceType: "gift", PreviewURL: "https://static.example.test/season-star-v2.png"},
	}

	panel := roomGiftBagGiftsFromResources(resources, []giftConfigData{liveV2}, 1)
	if len(panel) != 1 || panel[0].Name != "Season Star V1" || panel[0].PriceVersion != "v1" || panel[0].CoinPrice != 7 ||
		panel[0].Resource.PreviewURL != "https://static.example.test/season-star-v1.png" || panel[0].EntitlementID != "ent-pinned-star" ||
		panel[0].Source != "bag" || panel[0].RemainingQuantity != 2 {
		t.Fatalf("pinned bag panel drifted to live V2 catalog: %+v", panel)
	}
	// A disabled gift is absent from the live catalog, but the verified entitlement snapshot remains visible.
	panel = roomGiftBagGiftsFromResources(resources, nil, 1)
	if len(panel) != 1 || panel[0].Name != "Season Star V1" {
		t.Fatalf("disabled pinned gift disappeared from bag panel: %+v", panel)
	}
	if hidden := roomGiftBagGiftsFromResources(resources, nil, 2); len(hidden) != 0 {
		t.Fatalf("pinned gift leaked outside its pin-time region: %+v", hidden)
	}
	// Legacy inventory has no immutable gift bytes and therefore deliberately follows the current catalog.
	resources.Resources[0].SourceSnapshotId = ""
	resources.Resources[0].PinnedGiftConfigs = nil
	legacy := roomGiftBagGiftsFromResources(resources, []giftConfigData{liveV2}, 1)
	if len(legacy) != 1 || legacy[0].Name != "Mutable Star V2" || legacy[0].PriceVersion != "v2" {
		t.Fatalf("legacy bag entitlement did not fall back to live catalog: %+v", legacy)
	}
}

func TestCreateRoomDataIncludesExplicitIMGroupID(t *testing.T) {
	data := createRoomDataFromProto(&roomv1.CreateRoomResponse{
		Result: &roomv1.CommandResult{Applied: true, RoomVersion: 1, ServerTimeMs: 1700000000000},
		Room: &roomv1.RoomSnapshot{
			RoomId:          "room-create-im",
			OwnerUserId:     1001,
			Mode:            "voice",
			Status:          "active",
			ChatEnabled:     true,
			Version:         1,
			RoomShortId:     "1001",
			OnlineUsers:     []*roomv1.RoomUser{{UserId: 1001, Role: "owner"}},
			RoomExt:         map[string]string{"title": "Create IM", "cover_url": "cover"},
			VisibleRegionId: 10,
		},
	})

	if data.Room.RoomID != "room-create-im" || data.Room.IMGroupID != "room-create-im" {
		t.Fatalf("create response room must expose room_id and im_group_id: %+v", data.Room)
	}
	if data.IM.GroupID != "room-create-im" || !data.IM.NeedJoinGroup {
		t.Fatalf("create response must expose joinable im group: %+v", data.IM)
	}
	if !data.Result.Applied || data.ServerTimeMS != 1700000000000 {
		t.Fatalf("create response result mismatch: %+v", data)
	}
}

func TestRoomSeatDataIncludesGiftValue(t *testing.T) {
	seats := roomSeatDataFromSnapshot(&roomv1.RoomSnapshot{
		RoomId: "room-seat-gift-value",
		MicSeats: []*roomv1.SeatState{{
			SeatNo:    3,
			UserId:    1003,
			GiftValue: 367,
		}},
	})

	if len(seats) != 1 || seats[0].SeatNo != 3 || seats[0].UserID != "1003" || seats[0].GiftValue != 367 {
		t.Fatalf("seat data must expose gift_value: %+v", seats)
	}
}

func TestRoomProfileAgeFromBirth(t *testing.T) {
	now := time.Date(2026, time.May, 27, 12, 0, 0, 0, time.UTC)
	tests := []struct {
		name  string
		birth string
		want  int32
	}{
		{name: "birthday passed", birth: "2000-01-02", want: 26},
		{name: "birthday pending", birth: "2000-12-31", want: 25},
		{name: "today birthday", birth: "2000-05-27", want: 26},
		{name: "invalid", birth: "2000/01/02", want: 0},
		{name: "future", birth: "2030-01-01", want: 0},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if got := roomProfileAgeFromBirth(test.birth, now); got != test.want {
				t.Fatalf("age mismatch: got %d want %d", got, test.want)
			}
		})
	}
}

func TestRoomProfileCountryFlag(t *testing.T) {
	tests := []struct {
		name    string
		country string
		want    string
	}{
		{name: "upper code", country: "US", want: "🇺🇸"},
		{name: "lower code", country: "sg", want: "🇸🇬"},
		{name: "invalid length", country: "USA", want: ""},
		{name: "invalid character", country: "U1", want: ""},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if got := roomProfileCountryFlag(test.country); got != test.want {
				t.Fatalf("flag mismatch: got %q want %q", got, test.want)
			}
		})
	}
}

func TestRoomInitialProfileUserIDsIncludesRobotRoomUsers(t *testing.T) {
	snapshot := &roomv1.RoomSnapshot{
		OwnerUserId: 101,
		MicSeats:    []*roomv1.SeatState{{UserId: 102}},
		GiftRank:    []*roomv1.RankItem{{UserId: 201}},
		RoomExt: map[string]string{
			roomExtRobotRoomKey:    "true",
			roomExtRobotUserIDsKey: "301,302,101,bad,0",
		},
	}

	got := roomInitialProfileUserIDs(snapshot, 42, 10)
	want := []int64{101, 42, 102, 201, 301, 302}
	if len(got) != len(want) {
		t.Fatalf("profile user id length mismatch: got=%v want=%v", got, want)
	}
	for index := range want {
		if got[index] != want[index] {
			t.Fatalf("profile user ids mismatch: got=%v want=%v", got, want)
		}
	}
}

type shareRoomQueryClient struct {
	lastSnapshot *roomv1.GetRoomSnapshotRequest
	snapshotResp *roomv1.GetRoomSnapshotResponse
}

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

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

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

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

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

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

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

func (c *shareRoomQueryClient) GetRoomSnapshot(_ context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) {
	c.lastSnapshot = req
	if c.snapshotResp != nil {
		return c.snapshotResp, nil
	}
	return &roomv1.GetRoomSnapshotResponse{}, nil
}

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

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

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

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