package http

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"io"
	"net"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/redis/go-redis/v9"
	roomv1 "hyapp.local/api/proto/room/v1"
	"hyapp/services/gateway-service/internal/auth"
	"hyapp/services/gateway-service/internal/transport/http/giftlimit"
)

type fakeGiftCapacityLimiter struct {
	decision giftlimit.Decision
	err      error
	inputs   []giftlimit.Input
	releases int
}

func giftTestHostAuthority(handler *Handler) *Handler {
	// 成功路径显式装配 Host owner，并让空映射表示这些测试 target 不是 Host；生产不允许缺依赖时降级。
	handler.SetUserHostClient(&fakeUserHostClient{})
	return handler
}

func (f *fakeGiftCapacityLimiter) Acquire(_ context.Context, input giftlimit.Input) (giftlimit.Decision, error) {
	f.inputs = append(f.inputs, input)
	return f.decision, f.err
}

func (f *fakeGiftCapacityLimiter) Release(context.Context, giftlimit.Lease) error {
	f.releases++
	return nil
}

func TestGiftCapacityLimitWeightsUniqueTargetsAndReleasesInFlightSlot(t *testing.T) {
	roomClient := &fakeRoomClient{}
	limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}}
	handler := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/batch-send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-weighted","target_user_ids":[43,44,43,0],"gift_id":"rose","gift_count":1}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayTokenWithAppCode(t, "secret", 42, "huwaa"))
	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 len(limiter.inputs) != 1 {
		t.Fatalf("limiter acquire count mismatch: %d", len(limiter.inputs))
	}
	input := limiter.inputs[0]
	if input.AppCode != "huwaa" || input.UserID != 42 || input.RoomID != "room-1" || input.Weight != 2 || input.RequestID == "" {
		t.Fatalf("weighted limiter input mismatch: %+v", input)
	}
	if limiter.releases != 1 {
		t.Fatalf("successful acquire must release in-flight slot exactly once: %d", limiter.releases)
	}
	if roomClient.lastGift == nil || len(roomClient.lastGift.GetTargetUserIds()) != 2 {
		t.Fatalf("normal batch-send behavior changed: %+v", roomClient.lastGift)
	}
}

func TestGiftCapacityLimitPreservesSingleTargetWeight(t *testing.T) {
	limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}}
	handler := giftTestHostAuthority(NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-single","target_user_id":43,"gift_id":"rose","gift_count":1}`)))
	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 len(limiter.inputs) != 1 || limiter.inputs[0].Weight != 1 {
		t.Fatalf("legacy single-target request must consume one token: %+v", limiter.inputs)
	}
}

func TestGiftCapacityLimitWeightsGiftCountTimesUniqueTargets(t *testing.T) {
	limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}}
	handler := giftTestHostAuthority(NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/batch-send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-units","target_user_ids":[43,44,43],"gift_id":"rose","gift_count":99}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || len(limiter.inputs) != 1 || limiter.inputs[0].Weight != 198 {
		t.Fatalf("gift work units must be count*deduplicated targets: status=%d inputs=%+v body=%s", recorder.Code, limiter.inputs, recorder.Body.String())
	}
}

func TestGiftCapacityLimitRejectsExplosiveGiftUnitsBeforeRedis(t *testing.T) {
	targets := make([]int64, 0, 6)
	for userID := int64(43); userID < 49; userID++ {
		targets = append(targets, userID)
	}
	body, err := json.Marshal(map[string]any{
		"room_id":         "room-1",
		"command_id":      "cmd-too-many-units",
		"target_user_ids": targets,
		"gift_id":         "rose",
		"gift_count":      999,
	})
	if err != nil {
		t.Fatalf("marshal body: %v", err)
	}
	roomClient := &fakeRoomClient{}
	limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}}
	handler := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/batch-send", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusBadRequest || len(limiter.inputs) != 0 || roomClient.lastGift != nil {
		t.Fatalf("gift_count*targets above hard cap reached Redis/downstream: status=%d inputs=%+v gift=%+v", recorder.Code, limiter.inputs, roomClient.lastGift)
	}
}

func TestGiftCapacityLimitRejectsGiftCountAboveV2Contract(t *testing.T) {
	roomClient := &fakeRoomClient{}
	limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}}
	handler := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-count-overflow","target_user_id":43,"gift_id":"rose","gift_count":1000}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusBadRequest || len(limiter.inputs) != 0 || roomClient.lastGift != nil {
		t.Fatalf("gift_count above 999 reached Redis/downstream: status=%d inputs=%+v gift=%+v", recorder.Code, limiter.inputs, roomClient.lastGift)
	}
}

func TestSendGiftV2PreservesV1FieldsAndForwardsStableBatchIdentity(t *testing.T) {
	roomClient := &fakeRoomClient{sendGiftResp: &roomv1.SendGiftResponse{
		Result:           &roomv1.CommandResult{Applied: true, RoomVersion: 14},
		BillingReceiptId: "receipt-v2",
		CoinBalanceAfter: 900,
		V2: &roomv1.SendGiftResultV2{
			ApiVersion: 2, CommandId: "combo-1_b1", ComboSessionId: "combo-1", BatchSeq: 1,
			CommittedRoomVersion: 14, BillingReceiptId: "receipt-v2", CoinBalanceAfter: 900,
			SettledGiftCount: 9, SettledTargetUserIds: []int64{43}, OperationStatus: "committed",
		},
	}}
	handler := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v2/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"combo-1_b1","combo_session_id":"combo-1","batch_seq":1,"target_user_id":43,"gift_id":"rose","gift_count":9}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("V2 status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if roomClient.lastGift == nil || roomClient.lastGift.GetComboSessionId() != "combo-1" || roomClient.lastGift.GetBatchSeq() != 1 || roomClient.lastGift.GetMeta().GetCommandId() != "combo-1_b1" || roomClient.lastGift.GetDisplayMode() != "" {
		t.Fatalf("V2 stable batch identity was not forwarded: %+v", roomClient.lastGift)
	}
	var envelope struct {
		Data struct {
			BillingReceiptID string `json:"billing_receipt_id"`
			CoinBalanceAfter int64  `json:"coin_balance_after"`
			V2               struct {
				CommandID            string `json:"command_id"`
				CommittedRoomVersion int64  `json:"committed_room_version"`
			} `json:"v2"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode V2 response: %v", err)
	}
	if envelope.Data.BillingReceiptID != "receipt-v2" || envelope.Data.CoinBalanceAfter != 900 || envelope.Data.V2.CommandID != "combo-1_b1" || envelope.Data.V2.CommittedRoomVersion != 14 {
		t.Fatalf("V1 compatibility or nested V2 result missing: %+v body=%s", envelope, recorder.Body.String())
	}
}

func TestSendGiftV1RoutesPreserveTopLevelFieldsAndStripNestedV2(t *testing.T) {
	tests := []struct {
		name        string
		path        string
		body        string
		displayMode string
	}{
		{
			name: "single",
			path: "/api/v1/rooms/gift/send",
			body: `{"room_id":"room-1","command_id":"legacy-single","target_user_id":43,"gift_id":"rose","gift_count":1}`,
		},
		{
			name:        "batch",
			path:        "/api/v1/rooms/gift/batch-send",
			body:        `{"room_id":"room-1","command_id":"legacy-batch","target_user_ids":[43,44],"gift_id":"rose","gift_count":2}`,
			displayMode: "batch",
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			roomClient := &fakeRoomClient{sendGiftResp: &roomv1.SendGiftResponse{
				Result:                 &roomv1.CommandResult{Applied: true, RoomVersion: 14},
				BillingReceiptId:       "legacy-receipt",
				RoomHeat:               81,
				CoinBalanceAfter:       900,
				GiftIncomeBalanceAfter: 700,
				V2: &roomv1.SendGiftResultV2{
					ApiVersion: 2, CommandId: "internal-result", OperationStatus: "committed",
				},
			}}
			router := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001})).Routes(auth.NewVerifier("secret"))
			request := httptest.NewRequest(http.MethodPost, test.path, bytes.NewReader([]byte(test.body)))
			request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
			recorder := httptest.NewRecorder()

			router.ServeHTTP(recorder, request)

			if recorder.Code != http.StatusOK {
				t.Fatalf("V1 status mismatch: got=%d body=%s", recorder.Code, recorder.Body.String())
			}
			if roomClient.lastGift == nil || roomClient.lastGift.GetDisplayMode() != test.displayMode {
				t.Fatalf("V1 request behavior changed: %+v", roomClient.lastGift)
			}
			var envelope struct {
				Data map[string]json.RawMessage `json:"data"`
			}
			if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
				t.Fatalf("decode V1 response: %v", err)
			}
			if _, exists := envelope.Data["v2"]; exists {
				t.Fatalf("legacy V1 response exposed nested v2: %s", recorder.Body.String())
			}
			var receipt string
			var balance int64
			var roomHeat int64
			if err := json.Unmarshal(envelope.Data["billing_receipt_id"], &receipt); err != nil {
				t.Fatalf("decode billing_receipt_id: %v", err)
			}
			if err := json.Unmarshal(envelope.Data["coin_balance_after"], &balance); err != nil {
				t.Fatalf("decode coin_balance_after: %v", err)
			}
			if err := json.Unmarshal(envelope.Data["room_heat"], &roomHeat); err != nil {
				t.Fatalf("decode room_heat: %v", err)
			}
			if receipt != "legacy-receipt" || balance != 900 || roomHeat != 81 {
				t.Fatalf("V1 top-level fields changed: receipt=%q balance=%d heat=%d body=%s", receipt, balance, roomHeat, recorder.Body.String())
			}
		})
	}
}

func TestSendGiftV2RequiresComboIdentityBeforeDownstream(t *testing.T) {
	roomClient := &fakeRoomClient{}
	handler := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v2/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-no-combo","target_user_id":43,"gift_id":"rose","gift_count":1}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusBadRequest || roomClient.lastGift != nil {
		t.Fatalf("V2 without combo identity reached downstream: status=%d gift=%+v body=%s", recorder.Code, roomClient.lastGift, recorder.Body.String())
	}
}

func TestGiftCapacityLimitReturns429EnvelopeBeforeDownstreamCalls(t *testing.T) {
	for _, endpoint := range []struct {
		name string
		path string
		body string
	}{
		{name: "single", path: "/api/v1/rooms/gift/send", body: `{"room_id":"room-1","command_id":"cmd-denied-single","target_user_id":43,"gift_id":"rose","gift_count":1}`},
		{name: "batch", path: "/api/v1/rooms/gift/batch-send", body: `{"room_id":"room-1","command_id":"cmd-denied-batch","target_user_ids":[43,44],"gift_id":"rose","gift_count":1}`},
	} {
		t.Run(endpoint.name, func(t *testing.T) {
			roomClient := &fakeRoomClient{}
			profileClient := &fakeUserProfileClient{regionID: 1001}
			limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{RetryAfter: 275 * time.Millisecond}}
			handler := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, profileClient))
			handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter)
			router := handler.Routes(auth.NewVerifier("secret"))
			request := httptest.NewRequest(http.MethodPost, endpoint.path, bytes.NewReader([]byte(endpoint.body)))
			request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
			recorder := httptest.NewRecorder()

			router.ServeHTTP(recorder, request)

			if recorder.Code != http.StatusTooManyRequests {
				t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
			}
			if recorder.Header().Get("Retry-After") != "1" {
				t.Fatalf("Retry-After mismatch: %q", recorder.Header().Get("Retry-After"))
			}
			var envelope struct {
				Code      string `json:"code"`
				Message   string `json:"message"`
				RequestID string `json:"request_id"`
				Data      struct {
					RetryAfterMS int64 `json:"retry_after_ms"`
				} `json:"data"`
			}
			if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
				t.Fatalf("decode response failed: %v body=%s", err, recorder.Body.String())
			}
			if envelope.Code != "RATE_LIMITED" || envelope.Message != "rate limited" || envelope.RequestID == "" || envelope.Data.RetryAfterMS != 275 {
				t.Fatalf("rate-limit envelope mismatch: %+v", envelope)
			}
			if roomClient.lastGift != nil || profileClient.lastGet != nil || len(profileClient.batchRequests) != 0 {
				t.Fatalf("denied request reached downstream: gift=%+v profile_get=%+v profile_batches=%d", roomClient.lastGift, profileClient.lastGet, len(profileClient.batchRequests))
			}
			if limiter.releases != 0 {
				t.Fatalf("denied request must not release an unowned slot: %d", limiter.releases)
			}
		})
	}
}

func TestGiftCapacityLimitFailsClosedWhenRedisUnavailable(t *testing.T) {
	roomClient := &fakeRoomClient{}
	limiter := &fakeGiftCapacityLimiter{err: errors.New("redis unavailable")}
	handler := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-error","target_user_id":43,"gift_id":"rose","gift_count":1}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusBadGateway || roomClient.lastGift != nil {
		t.Fatalf("limiter backend failure must fail closed: status=%d body=%s gift=%+v", recorder.Code, recorder.Body.String(), roomClient.lastGift)
	}
}

func TestGiftCapacityLimitBlockedRedisFailsClosedQuickly(t *testing.T) {
	// net.Pipe 对 Redis 写入只读不回，模拟连接存在但服务端永久不返回 Lua 结果；真正的
	// RedisLimiter 必须靠自己的 operation deadline 结束，而不是依赖测试 fake 立即报错。
	redisClient := redis.NewClient(&redis.Options{
		Addr:                  "blocked-redis",
		MaxRetries:            -1,
		ReadTimeout:           time.Second,
		WriteTimeout:          time.Second,
		ContextTimeoutEnabled: true,
		Dialer: func(context.Context, string, string) (net.Conn, error) {
			clientConn, serverConn := net.Pipe()
			go func() {
				defer serverConn.Close()
				_, _ = io.Copy(io.Discard, serverConn)
			}()
			return clientConn, nil
		},
	})
	defer redisClient.Close()
	limiter := giftlimit.NewRedisLimiter(redisClient, giftlimit.Config{RedisOperationTimeout: 50 * time.Millisecond})
	roomClient := &fakeRoomClient{}
	handler := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-blocked-redis","target_user_id":43,"gift_id":"rose","gift_count":1}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()
	startedAt := time.Now()

	router.ServeHTTP(recorder, request)
	elapsed := time.Since(startedAt)

	if recorder.Code != http.StatusBadGateway || roomClient.lastGift != nil || elapsed < 35*time.Millisecond || elapsed > 500*time.Millisecond {
		t.Fatalf("blocked Redis must fail closed inside short deadline: status=%d elapsed=%s body=%s gift=%+v", recorder.Code, elapsed, recorder.Body.String(), roomClient.lastGift)
	}
}

func TestGiftCapacityLimitRejectsTooManyUniqueTargetsBeforeRedis(t *testing.T) {
	uniqueTargets := make([]int64, 0, 31)
	for userID := int64(1); userID <= 31; userID++ {
		uniqueTargets = append(uniqueTargets, userID)
	}
	body, err := json.Marshal(map[string]any{
		"room_id":         "room-1",
		"command_id":      "cmd-too-many",
		"target_user_ids": uniqueTargets,
		"gift_id":         "rose",
		"gift_count":      1,
	})
	if err != nil {
		t.Fatalf("marshal body: %v", err)
	}
	roomClient := &fakeRoomClient{}
	limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}}
	handler := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/batch-send", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusBadRequest || len(limiter.inputs) != 0 || roomClient.lastGift != nil {
		t.Fatalf("31 unique targets must fail before Redis/downstream: status=%d body=%s inputs=%+v gift=%+v", recorder.Code, recorder.Body.String(), limiter.inputs, roomClient.lastGift)
	}
}

func TestGiftCapacityLimitRejectsUnsupportedTargetTypeBeforeRedis(t *testing.T) {
	roomClient := &fakeRoomClient{}
	limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}}
	handler := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-unsupported","target_type":"all_mic","gift_id":"rose","gift_count":1}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusBadRequest || len(limiter.inputs) != 0 || roomClient.lastGift != nil {
		t.Fatalf("unsupported target_type must fail before Redis/downstream: status=%d body=%s inputs=%+v gift=%+v", recorder.Code, recorder.Body.String(), limiter.inputs, roomClient.lastGift)
	}
}

func TestGiftCapacityLimitAppliesMaximumAfterTargetDeduplication(t *testing.T) {
	targets := make([]int64, 0, 31)
	for userID := int64(1); userID <= 30; userID++ {
		targets = append(targets, userID)
	}
	targets = append(targets, 30)
	body, err := json.Marshal(map[string]any{
		"room_id":         "room-1",
		"command_id":      "cmd-dedup-maximum",
		"target_user_ids": targets,
		"gift_id":         "rose",
		"gift_count":      1,
	})
	if err != nil {
		t.Fatalf("marshal body: %v", err)
	}
	limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{RetryAfter: 10 * time.Millisecond}}
	handler := giftTestHostAuthority(NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	handler.SetGiftCapacityProtection(true, 5*time.Second, 30, limiter)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/batch-send", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusTooManyRequests || len(limiter.inputs) != 1 || limiter.inputs[0].Weight != 30 {
		t.Fatalf("duplicate target must not trip max_targets_per_request: status=%d body=%s inputs=%+v", recorder.Code, recorder.Body.String(), limiter.inputs)
	}
}

func TestGiftRequestTotalDeadlineReleasesInFlightLease(t *testing.T) {
	roomClient := &fakeRoomClient{sendGiftWaitForContext: true}
	limiter := &fakeGiftCapacityLimiter{decision: giftlimit.Decision{Allowed: true}}
	handler := giftTestHostAuthority(NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}))
	handler.SetGiftCapacityProtection(true, 40*time.Millisecond, 30, limiter)
	router := handler.Routes(auth.NewVerifier("secret"))
	request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-timeout","target_user_id":43,"gift_id":"rose","gift_count":1}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	recorder := httptest.NewRecorder()
	startedAt := time.Now()

	router.ServeHTTP(recorder, request)
	elapsed := time.Since(startedAt)

	if recorder.Code == http.StatusOK || elapsed < 30*time.Millisecond || elapsed > 300*time.Millisecond {
		t.Fatalf("gift orchestration must honor one total deadline: status=%d elapsed=%s body=%s", recorder.Code, elapsed, recorder.Body.String())
	}
	if roomClient.lastGift == nil || limiter.releases != 1 {
		t.Fatalf("timed-out request must reach room once and release lease: gift=%+v releases=%d", roomClient.lastGift, limiter.releases)
	}
}
