package http

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"io"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strconv"
	"testing"
	"time"

	gamev1 "hyapp.local/api/proto/game/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	"hyapp/services/gateway-service/internal/auth"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
)

func TestListGamesUsesAuthenticatedUserRegionAndReturnsEnvelope(t *testing.T) {
	gameClient := &fakeGatewayGameClient{listResp: &gamev1.ListGamesResponse{
		Games: []*gamev1.AppGame{{
			GameId:       "demo_rocket_001",
			PlatformCode: "demo",
			NameKey:      "game.demo_rocket_001.name",
			Name:         "Rocket",
			LaunchMode:   "h5_popup",
			Orientation:  "portrait",
			Enabled:      true,
		}},
		ServerTimeMs: 1700000000000,
	}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodGet, "/api/v1/games?scene=voice_room&room_id=room-1", nil)
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-App-Language", "en")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastList.GetUserId() != 42 || gameClient.lastList.GetRegionId() != 1001 || gameClient.lastList.GetScene() != "voice_room" || gameClient.lastList.GetRoomId() != "room-1" {
		t.Fatalf("ListGames request mismatch: %+v", gameClient.lastList)
	}
	var envelope httpkit.ResponseEnvelope
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	data := envelope.Data.(map[string]any)
	games := data["games"].([]any)
	first := games[0].(map[string]any)
	if first["platform_code"] != "demo" || first["game_id"] != "demo_rocket_001" {
		t.Fatalf("game response mismatch: %+v", first)
	}
}

func TestListRecentGamesUsesAuthenticatedUserRegionAndLimit(t *testing.T) {
	gameClient := &fakeGatewayGameClient{recentResp: &gamev1.ListGamesResponse{
		Games: []*gamev1.AppGame{{
			GameId:       "viva_2",
			PlatformCode: "vivagames",
			Name:         "Rich Forever",
			LaunchMode:   "h5_popup",
			Orientation:  "portrait",
			Enabled:      true,
		}},
		ServerTimeMs: 1700000000000,
	}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodGet, "/api/v1/games/recent?scene=voice_room&room_id=room-1&limit=4", nil)
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-App-Language", "en")
	request.Header.Set("X-App-Platform", "android")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastRecent.GetUserId() != 42 || gameClient.lastRecent.GetRegionId() != 1001 || gameClient.lastRecent.GetScene() != "voice_room" || gameClient.lastRecent.GetRoomId() != "room-1" || gameClient.lastRecent.GetPageSize() != 4 || gameClient.lastRecent.GetClientPlatform() != "android" {
		t.Fatalf("ListRecentGames request mismatch: %+v", gameClient.lastRecent)
	}
	var envelope httpkit.ResponseEnvelope
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	data := envelope.Data.(map[string]any)
	games := data["games"].([]any)
	first := games[0].(map[string]any)
	if first["platform_code"] != "vivagames" || first["game_id"] != "viva_2" {
		t.Fatalf("recent game response mismatch: %+v", first)
	}
}

func TestListExploreWinnersReturnsSquareItems(t *testing.T) {
	gameClient := &fakeGatewayGameClient{exploreWinnersResp: &gamev1.ListExploreWinnersResponse{
		Winners: []*gamev1.ExploreWinner{{
			WinId:       "win-1",
			GameCode:    "game_dice",
			GameId:      "dice",
			UserId:      7001,
			DisplayName: "name",
			AvatarUrl:   "https://cdn.example/avatar.png",
			CoinAmount:  306000,
			WonAtMs:     1700000000000,
		}},
		ServerTimeMs: 1700000005000,
	}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodGet, "/api/v1/games/explore-winners?limit=20", nil)
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-App-Code", "lalu")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastExploreWinners.GetMeta().GetActorUserId() != 42 || gameClient.lastExploreWinners.GetMeta().GetAppCode() != "lalu" || gameClient.lastExploreWinners.GetPageSize() != 20 {
		t.Fatalf("ListExploreWinners request mismatch: %+v", gameClient.lastExploreWinners)
	}
	var envelope httpkit.ResponseEnvelope
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	data := envelope.Data.(map[string]any)
	items := data["items"].([]any)
	first := items[0].(map[string]any)
	if first["game_code"] != "game_dice" || first["user_id"] != "7001" || first["coin_amount"].(float64) != 306000 {
		t.Fatalf("explore winner response mismatch: %+v", first)
	}
	if first["display_name"] != "user-7001" || first["avatar_url"] != "https://cdn.example/avatar.png" {
		t.Fatalf("explore winner response mismatch: %+v", first)
	}
}

func TestGetBridgeScriptReturnsUniversalScriptConfig(t *testing.T) {
	gameClient := &fakeGatewayGameClient{bridgeResp: &gamev1.GetBridgeScriptResponse{
		Config: &gamev1.GameBridgeScriptConfig{
			BridgeScriptUrl:     "https://cdn.example.test/game-bridge.js",
			BridgeScriptVersion: "20260608.1",
			BridgeScriptSha256:  "abc",
		},
		ServerTimeMs: 1700000000000,
	}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodGet, "/api/v1/games/bridge-script", nil)
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastBridge.GetMeta().GetActorUserId() != 42 {
		t.Fatalf("GetBridgeScript request mismatch: %+v", gameClient.lastBridge)
	}
	var envelope httpkit.ResponseEnvelope
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	data := envelope.Data.(map[string]any)
	if data["bridge_script_url"] != "https://cdn.example.test/game-bridge.js" || data["bridgeScriptUrl"] != "https://cdn.example.test/game-bridge.js" || data["bridge_script_version"] != "20260608.1" || data["bridge_script_sha256"] != "abc" {
		t.Fatalf("bridge script response mismatch: %+v", data)
	}
}

func TestLaunchGameForwardsDisplayUserID(t *testing.T) {
	gameClient := &fakeGatewayGameClient{launchResp: &gamev1.LaunchGameResponse{
		SessionId:   "game_sess_1",
		LaunchUrl:   "https://provider.example/h5?session_token=token",
		ExpiresAtMs: 1700000900000,
		Orientation: "portrait",
	}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	accessToken := signGatewayToken(t, "secret", 42)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/games/demo_rocket_001/launch", bytes.NewReader([]byte(`{"scene":"voice_room","room_id":"room-1"}`)))
	request.Header.Set("Authorization", "Bearer "+accessToken)
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastLaunch.GetGameId() != "demo_rocket_001" || gameClient.lastLaunch.GetDisplayUserId() != "100001" || gameClient.lastLaunch.GetRoomId() != "room-1" || gameClient.lastLaunch.GetAccessToken() != accessToken {
		t.Fatalf("LaunchGame request mismatch: %+v", gameClient.lastLaunch)
	}
	providerToken := gameClient.lastLaunch.GetProviderToken()
	claims, err := auth.NewVerifier("secret").VerifyGameCallbackToken("Bearer " + providerToken)
	if err != nil || len(providerToken) > 512 || claims.UserID != 42 || claims.AppCode != "lalu" {
		t.Fatalf("provider token mismatch: length=%d claims=%+v err=%v", len(providerToken), claims, err)
	}
}

func TestDiceMatchRenewsAccessTokenWhenTokenNearExpiry(t *testing.T) {
	userClient := &fakeUserAuthClient{}
	gameClient := &fakeGatewayGameClient{}
	handler := NewHandlerWithClients(&fakeRoomClient{}, userClient, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/games/dice/match", bytes.NewReader([]byte(`{"game_id":"dice","stake_coin":100}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayTokenWithExpiresAt(t, "secret", 42, true, time.Now().Add(2*time.Minute)))
	request.Header.Set("X-Device-ID", "dev-dice")
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if userClient.lastAppHeartbeat == nil || userClient.lastAppHeartbeat.GetUserId() != 42 || userClient.lastAppHeartbeat.GetSessionId() != "sess-test" {
		t.Fatalf("dice match must renew with authenticated user/session: %+v", userClient.lastAppHeartbeat)
	}
	if userClient.lastAppHeartbeat.GetMeta().GetDeviceId() != "dev-dice" {
		t.Fatalf("dice match must forward device meta during token renew: %+v", userClient.lastAppHeartbeat.GetMeta())
	}
	if recorder.Header().Get("X-Hyapp-Access-Token") != "access-heartbeat" || recorder.Header().Get("X-Hyapp-Expires-In") != strconv.FormatInt(testAppAccessTokenTTLSec, 10) {
		t.Fatalf("dice match must expose renewed token headers: %+v", recorder.Header())
	}
}

func TestRPSConfigUsesRockSelfGame(t *testing.T) {
	gameClient := &fakeGatewayGameClient{}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodGet, "/api/v1/games/rps/config?game_id=rock", nil)
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastGetDiceConfig == nil || gameClient.lastGetDiceConfig.GetGameId() != "rock" {
		t.Fatalf("rps config must read rock self game config, got %+v", gameClient.lastGetDiceConfig)
	}
}

func TestRPSMatchUsesRockSelfGameAndAuthenticatedRegion(t *testing.T) {
	gameClient := &fakeGatewayGameClient{}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/games/rps/match", bytes.NewReader([]byte(`{"game_id":"rock","stake_coin":8000,"gesture":"paper"}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastMatchDice == nil || gameClient.lastMatchDice.GetGameId() != "rock" || gameClient.lastMatchDice.GetUserId() != 42 || gameClient.lastMatchDice.GetRegionId() != 1001 || gameClient.lastMatchDice.GetStakeCoin() != 8000 || gameClient.lastMatchDice.GetRpsGesture() != "paper" {
		t.Fatalf("rps match request mismatch: %+v", gameClient.lastMatchDice)
	}
}

func TestRPSCreateUsesPreselectedGesture(t *testing.T) {
	gameClient := &fakeGatewayGameClient{}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/games/rps/matches", bytes.NewReader([]byte(`{"game_id":"rock","stake_coin":500,"gesture":"Scissors"}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastCreateDice == nil || gameClient.lastCreateDice.GetGameId() != "rock" || gameClient.lastCreateDice.GetRpsGesture() != "scissors" || gameClient.lastCreateDice.GetRegionId() != 1001 {
		t.Fatalf("rps create request mismatch: %+v", gameClient.lastCreateDice)
	}
}

func TestRPSMatchOperationsCarryRockBoundary(t *testing.T) {
	gameClient := &fakeGatewayGameClient{}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	token := "Bearer " + signGatewayToken(t, "secret", 42)

	// rps 的后续操作路径没有 body 里的 game_id 可依赖，必须由 gateway 根据路由固定传 rock 给 game-service 做 match 归属复核。
	requests := []struct {
		method string
		path   string
		body   string
		check  func()
	}{
		{
			method: http.MethodPost,
			path:   "/api/v1/games/rps/matches/match-rock-1/join",
			body:   `{"gesture":"scissors"}`,
			check: func() {
				if gameClient.lastJoinDice == nil || gameClient.lastJoinDice.GetGameId() != "rock" || gameClient.lastJoinDice.GetRpsGesture() != "scissors" {
					t.Fatalf("rps join must carry rock game_id: %+v", gameClient.lastJoinDice)
				}
			},
		},
		{
			method: http.MethodGet,
			path:   "/api/v1/games/rps/matches/match-rock-1",
			check: func() {
				if gameClient.lastGetDice == nil || gameClient.lastGetDice.GetGameId() != "rock" {
					t.Fatalf("rps get must carry rock game_id: %+v", gameClient.lastGetDice)
				}
			},
		},
		{
			method: http.MethodPost,
			path:   "/api/v1/games/rps/matches/match-rock-1/roll",
			body:   `{"gesture":"paper","game_id":"dice"}`,
			check: func() {
				if gameClient.lastRollDice == nil || gameClient.lastRollDice.GetGameId() != "rock" || gameClient.lastRollDice.GetRpsGesture() != "paper" {
					t.Fatalf("rps roll must ignore body game_id and carry rock: %+v", gameClient.lastRollDice)
				}
			},
		},
		{
			method: http.MethodPost,
			path:   "/api/v1/games/rps/matches/match-rock-1/cancel",
			check: func() {
				if gameClient.lastCancelDice == nil || gameClient.lastCancelDice.GetGameId() != "rock" {
					t.Fatalf("rps cancel must carry rock game_id: %+v", gameClient.lastCancelDice)
				}
			},
		},
	}
	for _, item := range requests {
		body := io.Reader(bytes.NewReader(nil))
		if item.body != "" {
			body = bytes.NewReader([]byte(item.body))
		}
		request := httptest.NewRequest(item.method, item.path, body)
		request.Header.Set("Authorization", token)
		request.Header.Set("Content-Type", "application/json")
		recorder := httptest.NewRecorder()

		router.ServeHTTP(recorder, request)

		if recorder.Code != http.StatusOK {
			t.Fatalf("%s %s status mismatch: got %d body=%s", item.method, item.path, recorder.Code, recorder.Body.String())
		}
		item.check()
	}
}

func TestDiceMatchOperationsCarryDiceBoundary(t *testing.T) {
	gameClient := &fakeGatewayGameClient{}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	token := "Bearer " + signGatewayToken(t, "secret", 42)

	requests := []struct {
		method string
		path   string
		check  func()
	}{
		{
			method: http.MethodPost,
			path:   "/api/v1/games/dice/matches/match-dice-1/join",
			check: func() {
				if gameClient.lastJoinDice == nil || gameClient.lastJoinDice.GetGameId() != "dice" {
					t.Fatalf("dice join must carry dice game_id: %+v", gameClient.lastJoinDice)
				}
			},
		},
		{
			method: http.MethodGet,
			path:   "/api/v1/games/dice/matches/match-dice-1",
			check: func() {
				if gameClient.lastGetDice == nil || gameClient.lastGetDice.GetGameId() != "dice" {
					t.Fatalf("dice get must carry dice game_id: %+v", gameClient.lastGetDice)
				}
			},
		},
		{
			method: http.MethodPost,
			path:   "/api/v1/games/dice/matches/match-dice-1/roll",
			check: func() {
				if gameClient.lastRollDice == nil || gameClient.lastRollDice.GetGameId() != "dice" {
					t.Fatalf("dice roll must carry dice game_id: %+v", gameClient.lastRollDice)
				}
			},
		},
		{
			method: http.MethodPost,
			path:   "/api/v1/games/dice/matches/match-dice-1/cancel",
			check: func() {
				if gameClient.lastCancelDice == nil || gameClient.lastCancelDice.GetGameId() != "dice" {
					t.Fatalf("dice cancel must carry dice game_id: %+v", gameClient.lastCancelDice)
				}
			},
		},
	}
	for _, item := range requests {
		request := httptest.NewRequest(item.method, item.path, nil)
		request.Header.Set("Authorization", token)
		recorder := httptest.NewRecorder()

		router.ServeHTTP(recorder, request)

		if recorder.Code != http.StatusOK {
			t.Fatalf("%s %s status mismatch: got %d body=%s", item.method, item.path, recorder.Code, recorder.Body.String())
		}
		item.check()
	}
}

func TestCreateRoomRPSChallengeUsesAuthenticatedUserAndRegion(t *testing.T) {
	gameClient := &fakeGatewayGameClient{roomRPSChallengeResp: &gamev1.RoomRPSChallengeResponse{
		Challenge: &gamev1.RoomRPSChallenge{
			ChallengeId:     "rps-1",
			RoomId:          "room-1",
			Status:          "pending",
			StakeGiftIdText: "Gifi-3",
			Initiator:       &gamev1.RoomRPSPlayer{UserId: 42, Gesture: "rock"},
		},
		ServerTimeMs: 1700000000000,
	}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/games/room-rps/challenges/create", bytes.NewReader([]byte(`{"room_id":"room-1","gift_id":"Gifi-3","gesture":"Rock"}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastCreateRoomRPS.GetUserId() != 42 || gameClient.lastCreateRoomRPS.GetRegionId() != 1001 || gameClient.lastCreateRoomRPS.GetRoomId() != "room-1" || gameClient.lastCreateRoomRPS.GetGiftIdText() != "Gifi-3" || gameClient.lastCreateRoomRPS.GetGiftId() != 0 || gameClient.lastCreateRoomRPS.GetGesture() != "rock" {
		t.Fatalf("CreateRoomRPSChallenge request mismatch: %+v", gameClient.lastCreateRoomRPS)
	}
	var envelope httpkit.ResponseEnvelope
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	data := envelope.Data.(map[string]any)
	challenge := data["challenge"].(map[string]any)
	if challenge["challenge_id"] != "rps-1" || challenge["stake_gift_id"] != "Gifi-3" {
		t.Fatalf("room rps response mismatch: %+v", challenge)
	}
}

func TestAcceptRoomRPSChallengeForwardsGestureOnly(t *testing.T) {
	gameClient := &fakeGatewayGameClient{roomRPSChallengeResp: &gamev1.RoomRPSChallengeResponse{
		Challenge: &gamev1.RoomRPSChallenge{
			ChallengeId: "rps-1",
			RoomId:      "room-1",
			Status:      "finished",
			Initiator:   &gamev1.RoomRPSPlayer{UserId: 41, Gesture: "rock", Result: "lose"},
			Challenger:  &gamev1.RoomRPSPlayer{UserId: 42, Gesture: "paper", Result: "win"},
		},
	}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/games/room-rps/challenges/rps-1/accept", bytes.NewReader([]byte(`{"gesture":"paper","gift_id":999}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastAcceptRoomRPS.GetUserId() != 42 || gameClient.lastAcceptRoomRPS.GetChallengeId() != "rps-1" || gameClient.lastAcceptRoomRPS.GetGesture() != "paper" {
		t.Fatalf("AcceptRoomRPSChallenge request mismatch: %+v", gameClient.lastAcceptRoomRPS)
	}
}

func TestGetRoomRPSChallengeEnrichesBothPlayerProfiles(t *testing.T) {
	gameClient := &fakeGatewayGameClient{roomRPSChallengeResp: &gamev1.RoomRPSChallengeResponse{
		Challenge: &gamev1.RoomRPSChallenge{
			ChallengeId: "rps-1",
			RoomId:      "room-1",
			Status:      "finished",
			Initiator:   &gamev1.RoomRPSPlayer{UserId: 41, Gesture: "rock", Result: "lose"},
			Challenger:  &gamev1.RoomRPSPlayer{UserId: 42, Gesture: "paper", Result: "win"},
		},
	}}
	profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
		41: &userv1.User{UserId: 41, DisplayUserId: "410001", Username: "Alice", Avatar: "https://cdn.example/alice.png"},
		42: &userv1.User{UserId: 42, DisplayUserId: "420001", Username: "Bob", Avatar: "https://cdn.example/bob.png"},
	}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodGet, "/api/v1/games/room-rps/challenges/rps-1", nil)
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	// 房内猜拳 IM 也需要同一套展示字段；HTTP 详情先锁住 gateway 返回的双方头像昵称，避免 Flutter 为基础展示再补拉资料。
	var envelope httpkit.ResponseEnvelope
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	challenge := envelope.Data.(map[string]any)["challenge"].(map[string]any)
	initiatorUser := challenge["initiator"].(map[string]any)["user"].(map[string]any)
	challengerUser := challenge["challenger"].(map[string]any)["user"].(map[string]any)
	if initiatorUser["nickname"] != "Alice" || initiatorUser["avatar"] != "https://cdn.example/alice.png" {
		t.Fatalf("initiator display profile mismatch: %+v", initiatorUser)
	}
	if challengerUser["nickname"] != "Bob" || challengerUser["avatar"] != "https://cdn.example/bob.png" {
		t.Fatalf("challenger display profile mismatch: %+v", challengerUser)
	}
}

func TestGameCallbackIsRawPublicResponse(t *testing.T) {
	gameClient := &fakeGatewayGameClient{callbackResp: &gamev1.CallbackResponse{RawBody: []byte(`{"code":0}`), ContentType: "application/json"}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/game-callbacks/demo/change_coin?nonce=1", bytes.NewReader([]byte(`{"provider_order_id":"p1"}`)))
	request.Header.Set("X-App-Code", "lalu")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || recorder.Body.String() != `{"code":0}` {
		t.Fatalf("callback response must not use app envelope: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastCallback.GetPlatformCode() != "demo" || gameClient.lastCallback.GetOperation() != "change_coin" || string(gameClient.lastCallback.GetRawBody()) != `{"provider_order_id":"p1"}` || gameClient.lastCallback.GetQuery()["nonce"] != "1" {
		t.Fatalf("callback request mismatch: %+v", gameClient.lastCallback)
	}
}

func TestZGameFixedCallbackRouteIsRawPublicResponse(t *testing.T) {
	gameClient := &fakeGatewayGameClient{callbackResp: &gamev1.CallbackResponse{RawBody: []byte(`{"code":0,"errmsg":"success"}`), ContentType: "application/json"}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/zgame/callback/api/server/login?nonce=1", bytes.NewReader([]byte(`{"open_id":"420001"}`)))
	request.Header.Set("X-App-Code", "lalu")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || recorder.Body.String() != `{"code":0,"errmsg":"success"}` {
		t.Fatalf("zgame callback response must not use app envelope: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastCallback.GetPlatformCode() != "zgame" || gameClient.lastCallback.GetOperation() != "server/login" || string(gameClient.lastCallback.GetRawBody()) != `{"open_id":"420001"}` || gameClient.lastCallback.GetQuery()["nonce"] != "1" {
		t.Fatalf("zgame callback request mismatch: %+v", gameClient.lastCallback)
	}
}

func TestAMGFixedCallbackRoutesUseAuthorizationTokenAppAndRawResponse(t *testing.T) {
	token, err := auth.NewVerifier("secret").IssueGameProviderToken("fami", 42, 24*time.Hour)
	if err != nil {
		t.Fatalf("IssueGameProviderToken failed: %v", err)
	}
	tests := []struct {
		name      string
		method    string
		path      string
		body      string
		operation string
	}{
		{name: "user", method: http.MethodGet, path: "/api/v1/amg/user?room=13", operation: "user"},
		{name: "settlement", method: http.MethodPost, path: "/api/v1/amg/settlement", body: `{"data":"aabb"}`, operation: "settlement"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gameClient := &fakeGatewayGameClient{callbackResp: &gamev1.CallbackResponse{RawBody: []byte(`{"code":0,"data":{},"msg":"success"}`), ContentType: "application/json"}}
			handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
			handler.SetGameClient(gameClient)
			router := handler.Routes(auth.NewVerifier("secret"))
			request := httptest.NewRequest(tt.method, tt.path, bytes.NewReader([]byte(tt.body)))
			// AMG 文档发送 token 原文而不是 Bearer；gateway 仍需验签后从 claims 解析真实 App。
			request.Header.Set("Authorization", token)
			recorder := httptest.NewRecorder()

			router.ServeHTTP(recorder, request)

			if recorder.Code != http.StatusOK || recorder.Body.String() != `{"code":0,"data":{},"msg":"success"}` {
				t.Fatalf("AMG response mismatch: status=%d body=%s", recorder.Code, recorder.Body.String())
			}
			if gameClient.lastCallback == nil || gameClient.lastCallback.GetPlatformCode() != "amg" || gameClient.lastCallback.GetOperation() != tt.operation || gameClient.lastCallback.GetMeta().GetAppCode() != "fami" || gameClient.lastCallback.GetMeta().GetActorUserId() != 42 {
				t.Fatalf("AMG callback mismatch: %+v", gameClient.lastCallback)
			}
			if tt.operation == "user" && (len(gameClient.lastCallback.GetRawBody()) != 0 || gameClient.lastCallback.GetQuery()["room"] != "13") {
				t.Fatalf("AMG user callback payload mismatch: %+v", gameClient.lastCallback)
			}
			if tt.operation == "settlement" && string(gameClient.lastCallback.GetRawBody()) != tt.body {
				t.Fatalf("AMG settlement body mismatch: %s", gameClient.lastCallback.GetRawBody())
			}
		})
	}
}

func TestGameCallbackUsesVerifiedTokenAppCodeFromBodyQueryAndHeader(t *testing.T) {
	token := signGatewayTokenWithAppCode(t, "secret", 42, "yumi")
	tests := []struct {
		name       string
		path       string
		body       string
		headerName string
	}{
		{
			name: "body js_code",
			path: "/api/v1/zgame/callback/api/server/login?nonce=1",
			body: `{"open_id":"420001","js_code":"` + token + `"}`,
		},
		{
			name: "query access_token",
			path: "/api/v1/game-callbacks/demo/change_coin?nonce=1&access_token=" + url.QueryEscape(token),
			body: `{"provider_order_id":"p1"}`,
		},
		{
			name:       "header access token",
			path:       "/api/v1/game-callbacks/demo/change_coin?nonce=1",
			body:       `{"provider_order_id":"p1"}`,
			headerName: "X-Hyapp-Access-Token",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gameClient := &fakeGatewayGameClient{callbackResp: &gamev1.CallbackResponse{RawBody: []byte(`{"code":0}`), ContentType: "application/json"}}
			handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
			handler.SetGameClient(gameClient)
			router := handler.Routes(auth.NewVerifier("secret"))
			request := httptest.NewRequest(http.MethodPost, tt.path, bytes.NewReader([]byte(tt.body)))
			request.Header.Set("X-App-Code", "lalu")
			if tt.headerName != "" {
				request.Header.Set(tt.headerName, token)
			}
			recorder := httptest.NewRecorder()

			router.ServeHTTP(recorder, request)

			if recorder.Code != http.StatusOK {
				t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
			}
			if gameClient.lastCallback == nil {
				t.Fatalf("callback was not forwarded")
			}
			if got := gameClient.lastCallback.GetMeta().GetAppCode(); got != "yumi" {
				t.Fatalf("callback app_code must come from verified token: got %q", got)
			}
			if got := gameClient.lastCallback.GetMeta().GetActorUserId(); got != 42 {
				t.Fatalf("callback actor user must come from verified token: got %d", got)
			}
		})
	}
}

func TestGameCallbackRejectsInvalidHyappToken(t *testing.T) {
	invalidToken := signGatewayTokenWithAppCode(t, "other-secret", 42, "yumi")
	gameClient := &fakeGatewayGameClient{callbackResp: &gamev1.CallbackResponse{RawBody: []byte(`{"code":0}`), ContentType: "application/json"}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/game-callbacks/demo/change_coin?nonce=1", bytes.NewReader([]byte(`{"token":"`+invalidToken+`"}`)))
	request.Header.Set("X-App-Code", "lalu")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusUnauthorized {
		t.Fatalf("invalid callback token must be rejected: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastCallback != nil {
		t.Fatalf("invalid token callback must not reach game-service: %+v", gameClient.lastCallback)
	}
}

func TestHotgameCompatFallbacksToHyappReyouCallback(t *testing.T) {
	body := `{"gameId":"101","uid":"420001","token":"hyapp-token","sign":"s"}`
	gameClient := &fakeGatewayGameClient{callbackResp: &gamev1.CallbackResponse{RawBody: []byte(`{"errorCode":0}`), ContentType: "application/json"}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/getUserInfo?nonce=1", bytes.NewReader([]byte(body)))
	request.Header.Set("X-App-Code", "lalu")
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || recorder.Body.String() != `{"errorCode":0}` {
		t.Fatalf("hotgame hyapp callback response mismatch: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if gameClient.lastCallback == nil || gameClient.lastCallback.GetPlatformCode() != "reyou" || gameClient.lastCallback.GetOperation() != "getUserInfo" || string(gameClient.lastCallback.GetRawBody()) != body || gameClient.lastCallback.GetQuery()["nonce"] != "1" {
		t.Fatalf("hotgame hyapp callback request mismatch: %+v", gameClient.lastCallback)
	}
}

func TestHotgameCompatProxiesChatapp3TokenWithoutGameCallback(t *testing.T) {
	token := chatapp3HotgameToken("v1", 1001, "LIKEI", 4102444800000, 1700000000000)
	body := `{"orderId":"o1","gameId":"13","roundId":"r1","uid":"1001","coin":12,"type":1,"token":"` + token + `","sign":"s"}`
	var gotHost string
	var gotPath string
	var gotQuery string
	var gotBody string
	var gotContentType string
	oldTransport := http.DefaultTransport
	http.DefaultTransport = roundTripFunc(func(request *http.Request) (*http.Response, error) {
		gotHost = request.URL.Host
		gotPath = request.URL.Path
		gotQuery = request.URL.RawQuery
		gotContentType = request.Header.Get("Content-Type")
		raw, _ := io.ReadAll(request.Body)
		gotBody = string(raw)
		header := http.Header{}
		header.Set("Content-Type", "application/json")
		return &http.Response{
			StatusCode: http.StatusOK,
			Header:     header,
			Body:       io.NopCloser(bytes.NewReader([]byte(`{"errorCode":0,"data":{"source":"chatapp3"}}`))),
			Request:    request,
		}, nil
	})
	defer func() { http.DefaultTransport = oldTransport }()
	gameClient := &fakeGatewayGameClient{callbackResp: &gamev1.CallbackResponse{RawBody: []byte(`{"errorCode":0}`), ContentType: "application/json"}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/updateBalance?nonce=1", bytes.NewReader([]byte(body)))
	request.Header.Set("X-App-Code", "lalu")
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || recorder.Body.String() != `{"errorCode":0,"data":{"source":"chatapp3"}}` {
		t.Fatalf("hotgame chatapp3 proxy response mismatch: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if gotHost != "jvapi.haiyihy.com" || gotPath != "/go/updateBalance" || gotQuery != "nonce=1" || gotBody != body || gotContentType != "application/json" {
		t.Fatalf("hotgame chatapp3 proxy request mismatch: host=%q path=%q query=%q content_type=%q body=%s", gotHost, gotPath, gotQuery, gotContentType, gotBody)
	}
	if gameClient.lastCallback != nil {
		t.Fatalf("chatapp3 token must not enter hyapp game callback: %+v", gameClient.lastCallback)
	}
}

func TestReyouGameCallbackProxiesChatapp3TokenWithoutGameCallback(t *testing.T) {
	token := chatapp3HotgameToken("v1", 99123456, "LIKEI", 4102444800000, 1700000000000)
	body := `{"gameId":"20","uid":"99123456","token":"` + token + `","sign":"s"}`
	var gotHost string
	var gotPath string
	var gotQuery string
	var gotBody string
	var gotContentType string
	oldTransport := http.DefaultTransport
	http.DefaultTransport = roundTripFunc(func(request *http.Request) (*http.Response, error) {
		gotHost = request.URL.Host
		gotPath = request.URL.Path
		gotQuery = request.URL.RawQuery
		gotContentType = request.Header.Get("Content-Type")
		raw, _ := io.ReadAll(request.Body)
		gotBody = string(raw)
		header := http.Header{}
		header.Set("Content-Type", "application/json")
		return &http.Response{
			StatusCode: http.StatusOK,
			Header:     header,
			Body:       io.NopCloser(bytes.NewReader([]byte(`{"errorCode":0,"data":{"coin":1000}}`))),
			Request:    request,
		}, nil
	})
	defer func() { http.DefaultTransport = oldTransport }()
	gameClient := &fakeGatewayGameClient{callbackResp: &gamev1.CallbackResponse{RawBody: []byte(`{"errorCode":0}`), ContentType: "application/json"}}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
	handler.SetGameClient(gameClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/game-callbacks/reyou/getUserInfo?nonce=1", bytes.NewReader([]byte(body)))
	request.Header.Set("X-App-Code", "lalu")
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || recorder.Body.String() != `{"errorCode":0,"data":{"coin":1000}}` {
		t.Fatalf("reyou chatapp3 proxy response mismatch: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if gotHost != "jvapi.haiyihy.com" || gotPath != "/go/getUserInfo" || gotQuery != "nonce=1" || gotBody != body || gotContentType != "application/json" {
		t.Fatalf("reyou chatapp3 proxy request mismatch: host=%q path=%q query=%q content_type=%q body=%s", gotHost, gotPath, gotQuery, gotContentType, gotBody)
	}
	if gameClient.lastCallback != nil {
		t.Fatalf("reyou chatapp3 token must not enter hyapp game callback: %+v", gameClient.lastCallback)
	}
}

func chatapp3HotgameToken(version string, userID int64, sysOrigin string, expireMs int64, releaseMs int64) string {
	payload := url.QueryEscape(version + ":" + strconv.FormatInt(userID, 10) + ":" + sysOrigin + ":" + strconv.FormatInt(expireMs, 10) + ":" + strconv.FormatInt(releaseMs, 10))
	return "sign." + base64.StdEncoding.EncodeToString([]byte(payload))
}

type roundTripFunc func(*http.Request) (*http.Response, error)

func (fn roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
	return fn(request)
}

type fakeGatewayGameClient struct {
	listResp             *gamev1.ListGamesResponse
	recentResp           *gamev1.ListGamesResponse
	exploreWinnersResp   *gamev1.ListExploreWinnersResponse
	bridgeResp           *gamev1.GetBridgeScriptResponse
	launchResp           *gamev1.LaunchGameResponse
	callbackResp         *gamev1.CallbackResponse
	lastList             *gamev1.ListGamesRequest
	lastRecent           *gamev1.ListRecentGamesRequest
	lastExploreWinners   *gamev1.ListExploreWinnersRequest
	lastBridge           *gamev1.GetBridgeScriptRequest
	lastLaunch           *gamev1.LaunchGameRequest
	lastCallback         *gamev1.CallbackRequest
	lastGetDiceConfig    *gamev1.GetDiceConfigRequest
	lastMatchDice        *gamev1.MatchDiceRequest
	lastCreateDice       *gamev1.CreateDiceMatchRequest
	lastJoinDice         *gamev1.JoinDiceMatchRequest
	lastGetDice          *gamev1.GetDiceMatchRequest
	lastRollDice         *gamev1.RollDiceMatchRequest
	lastCancelDice       *gamev1.CancelDiceMatchRequest
	roomRPSChallengeResp *gamev1.RoomRPSChallengeResponse
	lastListRoomRPS      *gamev1.ListRoomRPSChallengesRequest
	lastCreateRoomRPS    *gamev1.CreateRoomRPSChallengeRequest
	lastAcceptRoomRPS    *gamev1.AcceptRoomRPSChallengeRequest
	lastGetRoomRPS       *gamev1.GetRoomRPSChallengeRequest
}

func (f *fakeGatewayGameClient) ListGames(_ context.Context, req *gamev1.ListGamesRequest) (*gamev1.ListGamesResponse, error) {
	f.lastList = req
	if f.listResp != nil {
		return f.listResp, nil
	}
	return &gamev1.ListGamesResponse{}, nil
}

func (f *fakeGatewayGameClient) ListRecentGames(_ context.Context, req *gamev1.ListRecentGamesRequest) (*gamev1.ListGamesResponse, error) {
	f.lastRecent = req
	if f.recentResp != nil {
		return f.recentResp, nil
	}
	return &gamev1.ListGamesResponse{}, nil
}

func (f *fakeGatewayGameClient) ListExploreWinners(_ context.Context, req *gamev1.ListExploreWinnersRequest) (*gamev1.ListExploreWinnersResponse, error) {
	f.lastExploreWinners = req
	if f.exploreWinnersResp != nil {
		return f.exploreWinnersResp, nil
	}
	return &gamev1.ListExploreWinnersResponse{}, nil
}

func (f *fakeGatewayGameClient) GetBridgeScript(_ context.Context, req *gamev1.GetBridgeScriptRequest) (*gamev1.GetBridgeScriptResponse, error) {
	f.lastBridge = req
	if f.bridgeResp != nil {
		return f.bridgeResp, nil
	}
	return &gamev1.GetBridgeScriptResponse{}, nil
}

func (f *fakeGatewayGameClient) LaunchGame(_ context.Context, req *gamev1.LaunchGameRequest) (*gamev1.LaunchGameResponse, error) {
	f.lastLaunch = req
	if f.launchResp != nil {
		return f.launchResp, nil
	}
	return &gamev1.LaunchGameResponse{}, nil
}

func (f *fakeGatewayGameClient) GetDiceConfig(_ context.Context, req *gamev1.GetDiceConfigRequest) (*gamev1.DiceConfigResponse, error) {
	f.lastGetDiceConfig = req
	return &gamev1.DiceConfigResponse{Config: &gamev1.DiceConfig{GameId: req.GetGameId()}}, nil
}

func (f *fakeGatewayGameClient) MatchDice(_ context.Context, req *gamev1.MatchDiceRequest) (*gamev1.DiceMatchResponse, error) {
	f.lastMatchDice = req
	return &gamev1.DiceMatchResponse{}, nil
}

func (f *fakeGatewayGameClient) CreateDiceMatch(_ context.Context, req *gamev1.CreateDiceMatchRequest) (*gamev1.DiceMatchResponse, error) {
	f.lastCreateDice = req
	return &gamev1.DiceMatchResponse{}, nil
}

func (f *fakeGatewayGameClient) JoinDiceMatch(_ context.Context, req *gamev1.JoinDiceMatchRequest) (*gamev1.DiceMatchResponse, error) {
	f.lastJoinDice = req
	return &gamev1.DiceMatchResponse{}, nil
}

func (f *fakeGatewayGameClient) GetDiceMatch(_ context.Context, req *gamev1.GetDiceMatchRequest) (*gamev1.DiceMatchResponse, error) {
	f.lastGetDice = req
	return &gamev1.DiceMatchResponse{}, nil
}

func (f *fakeGatewayGameClient) RollDiceMatch(_ context.Context, req *gamev1.RollDiceMatchRequest) (*gamev1.DiceMatchResponse, error) {
	f.lastRollDice = req
	return &gamev1.DiceMatchResponse{}, nil
}

func (f *fakeGatewayGameClient) CancelDiceMatch(_ context.Context, req *gamev1.CancelDiceMatchRequest) (*gamev1.DiceMatchResponse, error) {
	f.lastCancelDice = req
	return &gamev1.DiceMatchResponse{}, nil
}

func (f *fakeGatewayGameClient) GetRoomRPSConfig(context.Context, *gamev1.GetRoomRPSConfigRequest) (*gamev1.RoomRPSConfigResponse, error) {
	return &gamev1.RoomRPSConfigResponse{}, nil
}

func (f *fakeGatewayGameClient) ListRoomRPSChallenges(_ context.Context, req *gamev1.ListRoomRPSChallengesRequest) (*gamev1.ListRoomRPSChallengesResponse, error) {
	f.lastListRoomRPS = req
	return &gamev1.ListRoomRPSChallengesResponse{}, nil
}

func (f *fakeGatewayGameClient) CreateRoomRPSChallenge(_ context.Context, req *gamev1.CreateRoomRPSChallengeRequest) (*gamev1.RoomRPSChallengeResponse, error) {
	f.lastCreateRoomRPS = req
	if f.roomRPSChallengeResp != nil {
		return f.roomRPSChallengeResp, nil
	}
	return &gamev1.RoomRPSChallengeResponse{}, nil
}

func (f *fakeGatewayGameClient) AcceptRoomRPSChallenge(_ context.Context, req *gamev1.AcceptRoomRPSChallengeRequest) (*gamev1.RoomRPSChallengeResponse, error) {
	f.lastAcceptRoomRPS = req
	if f.roomRPSChallengeResp != nil {
		return f.roomRPSChallengeResp, nil
	}
	return &gamev1.RoomRPSChallengeResponse{}, nil
}

func (f *fakeGatewayGameClient) GetRoomRPSChallenge(_ context.Context, req *gamev1.GetRoomRPSChallengeRequest) (*gamev1.RoomRPSChallengeResponse, error) {
	f.lastGetRoomRPS = req
	if f.roomRPSChallengeResp != nil {
		return f.roomRPSChallengeResp, nil
	}
	return &gamev1.RoomRPSChallengeResponse{}, nil
}

func (f *fakeGatewayGameClient) HandleCallback(_ context.Context, req *gamev1.CallbackRequest) (*gamev1.CallbackResponse, error) {
	f.lastCallback = req
	if f.callbackResp != nil {
		return f.callbackResp, nil
	}
	return &gamev1.CallbackResponse{RawBody: []byte(`{"code":0}`), ContentType: "application/json"}, nil
}
