package smoke_test

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"testing"
	"time"

	"hyapp/pkg/tencentim"
)

func TestRoomRPSLocalHTTPFlowWithRealTencentIM(t *testing.T) {
	if os.Getenv("ROOM_RPS_REAL_IM_FLOW_TEST") != "1" {
		t.Skip("set ROOM_RPS_REAL_IM_FLOW_TEST=1 to run the local room-rps HTTP flow with real Tencent IM")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
	defer cancel()

	cfg := roomRPSRealIMConfigFromEnv(t)
	imClient, err := tencentim.NewRESTClient(cfg)
	if err != nil {
		t.Fatalf("new real Tencent IM client failed: %v", err)
	}

	baseURL := strings.TrimRight(envOr("ROOM_RPS_BASE_URL", "http://127.0.0.1:13000/api/v1"), "/")
	appCode := envOr("ROOM_RPS_APP_CODE", "lalu")
	roomID := envOr("ROOM_RPS_ROOM_ID", fmt.Sprintf("room-rps-real-im-%d", time.Now().UnixMilli()))
	imGroupID := envOr("ROOM_RPS_IM_GROUP_ID", roomID)
	giftID := positiveInt64EnvOr(t, "ROOM_RPS_GIFT_ID", 10001)
	httpClient := &http.Client{Timeout: 15 * time.Second}

	initiator := quickCreateRoomRPSSmokeAccount(t, ctx, httpClient, baseURL, appCode, "room-rps-initiator")
	challenger := quickCreateRoomRPSSmokeAccount(t, ctx, httpClient, baseURL, appCode, "room-rps-challenger")
	if initiator.UserID == challenger.UserID {
		t.Fatalf("quick-created users must be different: initiator=%+v challenger=%+v", initiator, challenger)
	}

	// 真实 IM 是这个 smoke 的硬前置：先确保房间群存在并导入两个账号，避免后续 HTTP 成功但 IM 配置其实不可用。
	if err := imClient.EnsureGroup(ctx, tencentim.GroupSpec{
		GroupID:         imGroupID,
		Name:            "room rps real im smoke " + roomID,
		Type:            tencentim.DefaultGroupType,
		ApplyJoinOption: "FreeAccess",
	}); err != nil {
		t.Fatalf("ensure real Tencent IM room group failed: %v", err)
	}
	if shouldCleanupRoomRPSIMGroup() {
		if !strings.HasPrefix(imGroupID, "room-rps-real-im-") {
			t.Fatalf("ROOM_RPS_CLEANUP_IM_GROUP=1 only allows group id with room-rps-real-im- prefix, got %q", imGroupID)
		}
		t.Cleanup(func() {
			cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 20*time.Second)
			defer cleanupCancel()
			if err := imClient.DestroyGroup(cleanupCtx, imGroupID); err != nil {
				t.Errorf("destroy real Tencent IM room-rps smoke group failed: %v", err)
			}
		})
	}
	importRoomRPSIMAccount(t, ctx, imClient, initiator.UserID, initiator.Username)
	importRoomRPSIMAccount(t, ctx, imClient, challenger.UserID, challenger.Username)
	publishRoomRPSIMProbe(t, ctx, imClient, imGroupID, roomID)

	// /im/usersig 是 App 真正登录腾讯 IM SDK 前要调用的 gateway 接口；这里校验本地 gateway 和真实腾讯 IM SDKAppID 使用同一套配置。
	initiatorSig := requestRoomRPSUserSig(t, ctx, httpClient, baseURL, appCode, initiator.AccessToken, cfg.SDKAppID, initiator.UserID)
	challengerSig := requestRoomRPSUserSig(t, ctx, httpClient, baseURL, appCode, challenger.AccessToken, cfg.SDKAppID, challenger.UserID)
	if initiatorSig.UserID == challengerSig.UserID {
		t.Fatalf("initiator and challenger must be different IM identifiers: %s", initiatorSig.UserID)
	}

	config := requestRoomRPSConfig(t, ctx, httpClient, baseURL, appCode, initiator.AccessToken)
	if strings.ToLower(strings.TrimSpace(config.Config.Status)) != "active" {
		t.Fatalf("room-rps config must be open by default, got=%+v", config.Config)
	}
	assertRoomRPSGiftConfigured(t, config, giftID)

	listBefore := requestRoomRPSChallengeList(t, ctx, httpClient, baseURL, appCode, initiator.AccessToken, roomID)
	t.Logf("room-rps list before create: count=%d next_cursor=%q", len(listBefore.Challenges), listBefore.NextCursor)

	created := createRoomRPSChallenge(t, ctx, httpClient, baseURL, appCode, initiator.AccessToken, roomID, giftID, "rock")
	if created.Challenge.ChallengeID == "" {
		t.Fatalf("created room-rps challenge must contain challenge_id: %+v", created.Challenge)
	}
	if created.Challenge.Initiator.UserID != strconv.FormatInt(initiator.UserID, 10) {
		t.Fatalf("created challenge initiator mismatch: %+v", created.Challenge.Initiator)
	}
	if strings.TrimSpace(created.Challenge.Initiator.User.Nickname) == "" || strings.TrimSpace(created.Challenge.Initiator.User.Avatar) == "" {
		t.Fatalf("created challenge must include initiator nickname/avatar for IM UI: %+v", created.Challenge.Initiator.User)
	}

	accepted := acceptRoomRPSChallenge(t, ctx, httpClient, baseURL, appCode, challenger.AccessToken, created.Challenge.ChallengeID, "paper")
	if accepted.Challenge.Challenger.UserID != strconv.FormatInt(challenger.UserID, 10) {
		t.Fatalf("accepted challenge challenger mismatch: %+v", accepted.Challenge.Challenger)
	}
	if strings.TrimSpace(accepted.Challenge.Challenger.User.Nickname) == "" || strings.TrimSpace(accepted.Challenge.Challenger.User.Avatar) == "" {
		t.Fatalf("accepted challenge must include challenger nickname/avatar for IM UI: %+v", accepted.Challenge.Challenger.User)
	}

	detail := getRoomRPSChallenge(t, ctx, httpClient, baseURL, appCode, challenger.AccessToken, created.Challenge.ChallengeID)
	if detail.Challenge.ChallengeID != created.Challenge.ChallengeID {
		t.Fatalf("detail challenge id mismatch: got=%q want=%q", detail.Challenge.ChallengeID, created.Challenge.ChallengeID)
	}
	if detail.Challenge.Initiator.User.Nickname == "" || detail.Challenge.Challenger.User.Nickname == "" {
		t.Fatalf("detail must preserve both player display profiles: %+v", detail.Challenge)
	}

	t.Logf("room-rps real HTTP + IM smoke passed: base_url=%s room_id=%s im_group_id=%s challenge_id=%s", baseURL, roomID, imGroupID, created.Challenge.ChallengeID)
}

type roomRPSEnvelope struct {
	Code      string          `json:"code"`
	Message   string          `json:"message"`
	RequestID string          `json:"request_id"`
	Data      json.RawMessage `json:"data"`
}

type roomRPSUserSigData struct {
	SDKAppID   int64  `json:"sdk_app_id"`
	UserID     string `json:"user_id"`
	UserSig    string `json:"user_sig"`
	ExpireAtMS int64  `json:"expire_at_ms"`
}

type roomRPSQuickCreateData struct {
	UID          string           `json:"uid"`
	Account      string           `json:"account"`
	AccessToken  string           `json:"access_token"`
	RefreshToken string           `json:"refresh_token"`
	PasswordSet  bool             `json:"password_set"`
	Token        roomRPSAuthToken `json:"token"`
	Username     string           `json:"-"`
	UserID       int64            `json:"-"`
}

type roomRPSAuthToken struct {
	UserID      string `json:"user_id"`
	AccessToken string `json:"access_token"`
}

type roomRPSConfigData struct {
	Config       roomRPSConfig `json:"config"`
	ServerTimeMS int64         `json:"server_time_ms"`
}

type roomRPSConfig struct {
	Status             string             `json:"status"`
	ChallengeTimeoutMS int64              `json:"challenge_timeout_ms"`
	RevealCountdownMS  int64              `json:"reveal_countdown_ms"`
	StakeGifts         []roomRPSStakeGift `json:"stake_gifts"`
}

type roomRPSStakeGift struct {
	GiftID       string `json:"gift_id"`
	GiftIDNumber int64  `json:"gift_id_number"`
	Enabled      bool   `json:"enabled"`
	SortOrder    int32  `json:"sort_order"`
}

type roomRPSChallengeListData struct {
	Challenges   []roomRPSChallenge `json:"challenges"`
	NextCursor   string             `json:"next_cursor"`
	PageSize     int32              `json:"page_size"`
	ServerTimeMS int64              `json:"server_time_ms"`
}

type roomRPSChallengeData struct {
	Challenge    roomRPSChallenge `json:"challenge"`
	ServerTimeMS int64            `json:"server_time_ms"`
}

type roomRPSChallenge struct {
	ChallengeID      string        `json:"challenge_id"`
	RoomID           string        `json:"room_id"`
	Status           string        `json:"status"`
	StakeGiftID      string        `json:"stake_gift_id"`
	StakeGiftIDNum   int64         `json:"stake_gift_id_number"`
	Initiator        roomRPSPlayer `json:"initiator"`
	Challenger       roomRPSPlayer `json:"challenger"`
	WinnerUserID     string        `json:"winner_user_id"`
	SettlementStatus string        `json:"settlement_status"`
}

type roomRPSPlayer struct {
	UserID       string             `json:"user_id"`
	UserIDNumber int64              `json:"user_id_number"`
	Gesture      string             `json:"gesture"`
	Result       string             `json:"result"`
	User         roomRPSDisplayUser `json:"user"`
}

type roomRPSDisplayUser struct {
	UserID        string `json:"user_id"`
	DisplayUserID string `json:"display_user_id"`
	Nickname      string `json:"nickname"`
	Avatar        string `json:"avatar"`
}

func quickCreateRoomRPSSmokeAccount(t *testing.T, ctx context.Context, client *http.Client, baseURL string, appCode string, role string) roomRPSQuickCreateData {
	t.Helper()

	nowMS := time.Now().UnixMilli()
	username := fmt.Sprintf("%s-%d", role, nowMS)
	body := map[string]any{
		"password":        "room-rps-smoke-pass-123",
		"username":        username,
		"avatar":          fmt.Sprintf("https://cdn.example.com/%s.png", role),
		"gender":          "unknown",
		"country":         "SG",
		"device_id":       fmt.Sprintf("%s-device-%d", role, nowMS),
		"device":          "codex-smoke",
		"os_version":      "local",
		"birth":           "2000-01-01",
		"app_version":     "1.0.0",
		"build_number":    "1",
		"source":          "room_rps_real_im_smoke",
		"install_channel": "codex",
		"platform":        "android",
		"language":        "en",
		"timezone":        "Asia/Shanghai",
	}
	var data roomRPSQuickCreateData
	doRoomRPSJSON(t, ctx, client, http.MethodPost, baseURL+"/auth/account/quick-create", appCode, "", body, &data)
	data.Username = username
	userID, err := strconv.ParseInt(strings.TrimSpace(data.Token.UserID), 10, 64)
	if err != nil || userID <= 0 || strings.TrimSpace(data.AccessToken) == "" || !data.PasswordSet {
		t.Fatalf("quick-create account response invalid: %+v", data)
	}
	data.UserID = userID
	return data
}

func requestRoomRPSUserSig(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token string, sdkAppID int64, userID int64) roomRPSUserSigData {
	t.Helper()

	var data roomRPSUserSigData
	doRoomRPSJSON(t, ctx, client, http.MethodGet, baseURL+"/im/usersig", appCode, token, nil, &data)
	if data.SDKAppID != sdkAppID {
		t.Fatalf("usersig sdk_app_id mismatch: got=%d want=%d data=%+v", data.SDKAppID, sdkAppID, data)
	}
	if data.UserID != strconv.FormatInt(userID, 10) || strings.TrimSpace(data.UserSig) == "" || data.ExpireAtMS <= time.Now().UnixMilli() {
		t.Fatalf("usersig response mismatch for user %d: %+v", userID, data)
	}
	return data
}

func requestRoomRPSConfig(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token string) roomRPSConfigData {
	t.Helper()

	var data roomRPSConfigData
	doRoomRPSJSON(t, ctx, client, http.MethodGet, baseURL+"/games/room-rps/config", appCode, token, nil, &data)
	if data.Config.Status == "" {
		t.Fatalf("room-rps config response is empty: %+v", data)
	}
	return data
}

func requestRoomRPSChallengeList(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token, roomID string) roomRPSChallengeListData {
	t.Helper()

	var data roomRPSChallengeListData
	doRoomRPSJSON(t, ctx, client, http.MethodGet, baseURL+"/games/room-rps/challenges?room_id="+urlQueryEscape(roomID), appCode, token, nil, &data)
	return data
}

func createRoomRPSChallenge(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token, roomID string, giftID int64, gesture string) roomRPSChallengeData {
	t.Helper()

	body := map[string]any{"room_id": roomID, "gift_id": giftID, "gesture": gesture}
	var data roomRPSChallengeData
	doRoomRPSJSON(t, ctx, client, http.MethodPost, baseURL+"/games/room-rps/challenges/create", appCode, token, body, &data)
	return data
}

func acceptRoomRPSChallenge(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token, challengeID string, gesture string) roomRPSChallengeData {
	t.Helper()

	body := map[string]any{"gesture": gesture}
	var data roomRPSChallengeData
	doRoomRPSJSON(t, ctx, client, http.MethodPost, baseURL+"/games/room-rps/challenges/"+urlPathEscape(challengeID)+"/accept", appCode, token, body, &data)
	return data
}

func getRoomRPSChallenge(t *testing.T, ctx context.Context, client *http.Client, baseURL, appCode, token, challengeID string) roomRPSChallengeData {
	t.Helper()

	var data roomRPSChallengeData
	doRoomRPSJSON(t, ctx, client, http.MethodGet, baseURL+"/games/room-rps/challenges/"+urlPathEscape(challengeID), appCode, token, nil, &data)
	return data
}

func doRoomRPSJSON(t *testing.T, ctx context.Context, client *http.Client, method string, url string, appCode string, token string, body any, out any) {
	t.Helper()

	var payload io.Reader
	if body != nil {
		raw, err := json.Marshal(body)
		if err != nil {
			t.Fatalf("marshal %s %s body failed: %v", method, url, err)
		}
		payload = bytes.NewReader(raw)
	}
	req, err := http.NewRequestWithContext(ctx, method, url, payload)
	if err != nil {
		t.Fatalf("build %s %s request failed: %v", method, url, err)
	}
	if strings.TrimSpace(token) != "" {
		req.Header.Set("Authorization", "Bearer "+token)
	}
	req.Header.Set("X-App-Code", appCode)
	req.Header.Set("X-App-Language", "en")
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	resp, err := client.Do(req)
	if err != nil {
		t.Fatalf("%s %s failed: %v", method, url, err)
	}
	defer resp.Body.Close()
	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("read %s %s response failed: %v", method, url, err)
	}

	var envelope roomRPSEnvelope
	if err := json.Unmarshal(raw, &envelope); err != nil {
		t.Fatalf("decode %s %s envelope failed: status=%d body=%s err=%v", method, url, resp.StatusCode, string(raw), err)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 || envelope.Code != "OK" {
		t.Fatalf("%s %s returned non-OK: status=%d code=%s message=%s request_id=%s body=%s", method, url, resp.StatusCode, envelope.Code, envelope.Message, envelope.RequestID, string(raw))
	}
	if out != nil {
		if len(envelope.Data) == 0 {
			t.Fatalf("%s %s returned empty data: request_id=%s", method, url, envelope.RequestID)
		}
		if err := json.Unmarshal(envelope.Data, out); err != nil {
			t.Fatalf("decode %s %s data failed: request_id=%s data=%s err=%v", method, url, envelope.RequestID, string(envelope.Data), err)
		}
	}
}

func assertRoomRPSGiftConfigured(t *testing.T, data roomRPSConfigData, giftID int64) {
	t.Helper()

	for _, gift := range data.Config.StakeGifts {
		if gift.GiftIDNumber == giftID || gift.GiftID == strconv.FormatInt(giftID, 10) {
			if !gift.Enabled {
				t.Fatalf("ROOM_RPS_GIFT_ID is configured but disabled: %+v", gift)
			}
			return
		}
	}
	t.Fatalf("ROOM_RPS_GIFT_ID=%d not found in room-rps config: %+v", giftID, data.Config.StakeGifts)
}

func publishRoomRPSIMProbe(t *testing.T, ctx context.Context, client *tencentim.RESTClient, groupID string, roomID string) {
	t.Helper()

	nowMS := time.Now().UTC().UnixMilli()
	eventID := fmt.Sprintf("room_rps_real_im_probe:%d", nowMS)
	payload, err := json.Marshal(map[string]any{
		"event_id":       eventID,
		"event_type":     "room_rps_real_im_probe",
		"room_id":        roomID,
		"challenge_id":   "",
		"server_time_ms": nowMS,
		"challenge": map[string]any{
			"room_id": roomID,
			"status":  "probe",
		},
	})
	if err != nil {
		t.Fatalf("marshal real IM probe failed: %v", err)
	}

	// 这里真的调用腾讯云 IM REST 发送 TIMCustomElem；成功表示本地配置、admin UserSig、群 ID 和消息 payload 都被腾讯服务端接受。
	if err := client.PublishGroupCustomMessage(ctx, tencentim.CustomGroupMessage{
		GroupID:     groupID,
		EventID:     eventID,
		Desc:        "room_rps_real_im_probe",
		Ext:         "room_rps",
		PayloadJSON: payload,
	}); err != nil {
		t.Fatalf("publish real Tencent IM room-rps probe failed: %v", err)
	}
}

func importRoomRPSIMAccount(t *testing.T, ctx context.Context, client *tencentim.RESTClient, userID int64, nick string) {
	t.Helper()

	if err := client.ImportAccount(ctx, tencentim.AccountProfile{
		UserID:  userID,
		Nick:    nick,
		FaceURL: "https://cdn.example.com/room-rps-smoke.png",
	}); err != nil {
		t.Fatalf("import real Tencent IM account user_id=%d failed: %v", userID, err)
	}
}

func roomRPSRealIMConfigFromEnv(t *testing.T) tencentim.RESTConfig {
	t.Helper()

	sdkAppID := requiredPositiveInt64Env(t, "TENCENT_IM_SDK_APP_ID")
	ttl := time.Hour
	if rawTTL := strings.TrimSpace(os.Getenv("TENCENT_IM_ADMIN_USERSIG_TTL")); rawTTL != "" {
		parsed, err := time.ParseDuration(rawTTL)
		if err != nil || parsed <= 0 {
			t.Fatalf("TENCENT_IM_ADMIN_USERSIG_TTL is invalid")
		}
		ttl = parsed
	}
	return tencentim.RESTConfig{
		SDKAppID:        sdkAppID,
		SecretKey:       requiredRoomRPSEnv(t, "TENCENT_IM_SECRET_KEY"),
		AdminIdentifier: requiredRoomRPSEnv(t, "TENCENT_IM_ADMIN_IDENTIFIER"),
		AdminUserSigTTL: ttl,
		Endpoint:        envOr("TENCENT_IM_ENDPOINT", tencentim.DefaultEndpoint),
		GroupType:       tencentim.DefaultGroupType,
		HTTPClient:      &http.Client{Timeout: 10 * time.Second},
	}
}

func requiredRoomRPSEnv(t *testing.T, key string) string {
	t.Helper()

	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		t.Fatalf("%s is required for room-rps real IM flow test", key)
	}
	return value
}

func requiredPositiveInt64Env(t *testing.T, key string) int64 {
	t.Helper()

	value, err := strconv.ParseInt(requiredRoomRPSEnv(t, key), 10, 64)
	if err != nil || value <= 0 {
		t.Fatalf("%s must be a positive int64", key)
	}
	return value
}

func positiveInt64EnvOr(t *testing.T, key string, fallback int64) int64 {
	t.Helper()

	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		return fallback
	}
	parsed, err := strconv.ParseInt(value, 10, 64)
	if err != nil || parsed <= 0 {
		t.Fatalf("%s must be a positive int64", key)
	}
	return parsed
}

func envOr(key string, fallback string) string {
	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		return fallback
	}
	return value
}

func shouldCleanupRoomRPSIMGroup() bool {
	raw := strings.TrimSpace(os.Getenv("ROOM_RPS_CLEANUP_IM_GROUP"))
	return raw == "" || raw == "1"
}

func urlQueryEscape(value string) string {
	return url.QueryEscape(value)
}

func urlPathEscape(value string) string {
	return url.PathEscape(value)
}
