package resource

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"hyapp-admin-server/internal/appctx"
	"hyapp-admin-server/internal/integration/walletclient"
	"hyapp-admin-server/internal/middleware"
	walletv1 "hyapp.local/api/proto/wallet/v1"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/gin-gonic/gin"
)

func TestUpdateMP4ResourceLayoutsPreservesResourceFields(t *testing.T) {
	wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
		11: {
			AppCode:             "lalu",
			ResourceId:          11,
			ResourceCode:        "profile_card_star",
			ResourceType:        resourceTypeProfileCard,
			Name:                "星光资料卡",
			Status:              "active",
			Grantable:           true,
			GrantStrategy:       "extend_expiry",
			WalletAssetType:     "RESOURCE",
			WalletAssetAmount:   1,
			UsageScopes:         []string{resourceTypeProfileCard},
			AssetUrl:            "https://cdn.example.test/profile_card.png",
			PreviewUrl:          "https://cdn.example.test/profile_card_cover.png",
			AnimationUrl:        "https://cdn.example.test/profile_card.mp4?token=1",
			MetadataJson:        `{"profile_card_layout":{"source_width":1136,"source_height":1680,"color_content_width":750,"content_top":117,"content_bottom":1666,"content_height":1550,"detect_version":1}}`,
			SortOrder:           9,
			ManagerGrantEnabled: true,
			PriceType:           resourcePriceTypeCoin,
			CoinPrice:           100,
		},
	}}
	router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
	metadataJSON := `{
		"animation_format":"mp4",
		"mp4_alpha_layout":{
			"alpha_layout":"alpha_left_rgb_right",
			"video_w":1500,
			"video_h":1334,
			"rgb_frame":[750,0,750,1334],
			"alpha_frame":[0,0,750,1334],
			"confirmed":true,
			"detect_version":1
		},
		"debug":true
	}`
	body := fmt.Sprintf(`{"items":[{"resourceId":11,"metadataJson":%q}]}`, metadataJSON)

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPut, "/admin/resources/mp4-layouts/batch", strings.NewReader(body))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("batch mp4 layout update status mismatch: %d %s", recorder.Code, recorder.Body.String())
	}
	if len(wallet.updates) != 1 {
		t.Fatalf("expected one wallet update, got %d", len(wallet.updates))
	}
	update := wallet.updates[0]
	if update.GetResourceId() != 11 || update.GetName() != "星光资料卡" || update.GetStatus() != "active" || update.GetCoinPrice() != 100 {
		t.Fatalf("resource fields should be preserved: %+v", update)
	}
	if update.ManagerGrantEnabled == nil || !update.GetManagerGrantEnabled() || update.GetOperatorUserId() != 7 {
		t.Fatalf("operator and manager grant should be preserved: %+v", update)
	}
	if !strings.Contains(update.GetMetadataJson(), `"profile_card_layout"`) || !strings.Contains(update.GetMetadataJson(), `"mp4_alpha_layout"`) {
		t.Fatalf("profile card and mp4 metadata should both be kept: %s", update.GetMetadataJson())
	}
	if strings.Contains(update.GetMetadataJson(), "debug") {
		t.Fatalf("debug metadata should be dropped: %s", update.GetMetadataJson())
	}
}

func TestUpdateMP4ResourceLayoutsIgnoresNonMP4Resource(t *testing.T) {
	wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
		12: {
			AppCode:       "lalu",
			ResourceId:    12,
			ResourceCode:  "gift_rose",
			ResourceType:  "gift",
			Name:          "玫瑰",
			Status:        "active",
			AnimationUrl:  "https://cdn.example.test/gift.svga",
			MetadataJson:  "{}",
			GrantStrategy: "increase_quantity",
			PriceType:     resourcePriceTypeCoin,
			CoinPrice:     10,
		},
	}}
	router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
	body := fmt.Sprintf(`{"items":[{"resourceId":12,"metadataJson":%q}]}`, mp4AlphaLayoutMetadataJSON("alpha_left_rgb_right", true))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPut, "/admin/resources/mp4-layouts/batch", strings.NewReader(body))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("non-mp4 resource should be ignored, got %d %s", recorder.Code, recorder.Body.String())
	}
	if len(wallet.updates) != 0 {
		t.Fatalf("non-mp4 resource should not update wallet")
	}
}

func TestDeleteGiftOnlyCallsGiftConfigDelete(t *testing.T) {
	wallet := &mockResourceWallet{}
	router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodDelete, "/admin/gifts/rose", nil)
	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("delete gift status mismatch: %d %s", recorder.Code, recorder.Body.String())
	}
	if len(wallet.deletedGifts) != 1 {
		t.Fatalf("expected one gift delete request, got %d", len(wallet.deletedGifts))
	}
	deleteReq := wallet.deletedGifts[0]
	if deleteReq.GetAppCode() != "lalu" || deleteReq.GetGiftId() != "rose" || deleteReq.GetOperatorUserId() != 7 {
		t.Fatalf("delete gift request mismatch: %+v", deleteReq)
	}
}

func TestDeleteResourceCallsWalletDeleteResource(t *testing.T) {
	wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
		11: {AppCode: "lalu", ResourceId: 11, ResourceCode: "rose_resource", ResourceType: "gift", Name: "Rose"},
	}}
	router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodDelete, "/admin/resources/11", nil)
	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("delete resource status mismatch: %d %s", recorder.Code, recorder.Body.String())
	}
	if len(wallet.deletedResources) != 1 {
		t.Fatalf("expected one resource delete request, got %d", len(wallet.deletedResources))
	}
	deleteReq := wallet.deletedResources[0]
	if deleteReq.GetAppCode() != "lalu" || deleteReq.GetResourceId() != 11 || deleteReq.GetOperatorUserId() != 7 {
		t.Fatalf("delete resource request mismatch: %+v", deleteReq)
	}
}

func TestBatchDeleteResourcesCallsWalletBatchDelete(t *testing.T) {
	wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
		11: {AppCode: "lalu", ResourceId: 11, ResourceCode: "rose_resource", ResourceType: "gift", Name: "Rose"},
		12: {AppCode: "lalu", ResourceId: 12, ResourceCode: "ring_resource", ResourceType: "gift", Name: "Ring"},
	}}
	router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/admin/resources/batch-delete", strings.NewReader(`{"resourceIds":[11,12]}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("batch delete resources status mismatch: %d %s", recorder.Code, recorder.Body.String())
	}
	if len(wallet.batchDeletedResources) != 1 {
		t.Fatalf("expected one batch resource delete request, got %d", len(wallet.batchDeletedResources))
	}
	deleteReq := wallet.batchDeletedResources[0]
	if deleteReq.GetAppCode() != "lalu" || fmt.Sprint(deleteReq.GetResourceIds()) != "[11 12]" || deleteReq.GetOperatorUserId() != 7 {
		t.Fatalf("batch delete resource request mismatch: %+v", deleteReq)
	}
}

func TestBatchCreateGiftsCallsWalletBatchCreate(t *testing.T) {
	wallet := &mockResourceWallet{}
	router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))

	body := `{"items":[{"giftId":"rose","resourceId":11,"status":"active","name":"Rose","priceVersion":"default","giftTypeCode":"normal","chargeAssetType":"COIN","coinPrice":10,"regionIds":[0]},{"giftId":"ring","resourceId":12,"status":"active","name":"Ring","priceVersion":"default","giftTypeCode":"noble","vipLevel":5,"chargeAssetType":"COIN","coinPrice":20,"regionIds":[0]}]}`
	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/admin/gifts/batch", strings.NewReader(body))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusCreated {
		t.Fatalf("batch create gifts status mismatch: %d %s", recorder.Code, recorder.Body.String())
	}
	if len(wallet.batchCreatedGifts) != 1 {
		t.Fatalf("expected one batch gift create request, got %d", len(wallet.batchCreatedGifts))
	}
	createReq := wallet.batchCreatedGifts[0]
	if createReq.GetAppCode() != "lalu" || len(createReq.GetItems()) != 2 || createReq.GetItems()[0].GetGiftId() != "rose" || createReq.GetItems()[1].GetGiftId() != "ring" || createReq.GetItems()[1].GetVipLevel() != 5 {
		t.Fatalf("batch create gift request mismatch: %+v", createReq)
	}
}

func TestIdentityAutoGrantConfigRouteDoesNotHitResourceGroupID(t *testing.T) {
	router := newResourceHandlerTestRouter(New(&mockResourceWallet{}, nil, nil, time.Second, nil))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/admin/resource-groups/identity-auto-grant-config", nil)
	router.ServeHTTP(recorder, request)

	// 这里故意使用 nil store，让配置 handler 返回 500；如果路由被 :group_id 抢走，会变成 400 的 ID 参数错误。
	if recorder.Code != http.StatusInternalServerError {
		t.Fatalf("identity config route should hit config handler, got %d %s", recorder.Code, recorder.Body.String())
	}
	if strings.Contains(recorder.Body.String(), "ID 参数不正确") {
		t.Fatalf("identity config route should not be parsed as group_id: %s", recorder.Body.String())
	}
}

func TestGenericResourceGrantAllowsVIPTrialCard(t *testing.T) {
	wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
		99: {AppCode: "lalu", ResourceId: 99, ResourceCode: "vip_trial_5", ResourceType: resourceTypeVIPTrialCard, Name: "VIP5体验卡"},
	}}
	router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/admin/resource-grants/resource", strings.NewReader(`{"commandId":"grant-1","targetUserId":"318705991371722752","resourceId":99,"quantity":1,"durationMs":86400000,"reason":"manual"}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusCreated || len(wallet.grantRequests) != 1 || wallet.grantRequests[0].GetResourceId() != 99 {
		t.Fatalf("generic grant must forward vip trial card to wallet owner: status=%d body=%s requests=%+v", recorder.Code, recorder.Body.String(), wallet.grantRequests)
	}
}

func TestExternalResourceGrantRejectsIDOutsideManagerGrantCatalog(t *testing.T) {
	wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
		11: {AppCode: "lalu", ResourceId: 11, ResourceType: "badge", Status: "active", Grantable: true, ManagerGrantEnabled: true},
		12: {AppCode: "lalu", ResourceId: 12, ResourceType: "badge", Status: "active", Grantable: true, ManagerGrantEnabled: false},
	}}
	handler := New(wallet, nil, nil, time.Second, nil)
	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu"))
		c.Set(middleware.ContextRequestID, "external-grant-test")
		c.Set(middleware.ContextUserID, uint(7))
		c.Next()
	})
	router.POST("/external-grant", handler.GrantExternalResource)

	denied := httptest.NewRecorder()
	deniedRequest := httptest.NewRequest(http.MethodPost, "/external-grant", strings.NewReader(`{"commandId":"grant-denied","targetUserId":"318705991371722752","resourceId":12,"quantity":1,"durationMs":0,"reason":"manual"}`))
	deniedRequest.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(denied, deniedRequest)
	if denied.Code != http.StatusBadRequest || !strings.Contains(denied.Body.String(), "未开放外管发放") || len(wallet.grantRequests) != 0 {
		t.Fatalf("catalog bypass was not rejected: status=%d body=%s grants=%d", denied.Code, denied.Body.String(), len(wallet.grantRequests))
	}

	allowed := httptest.NewRecorder()
	allowedRequest := httptest.NewRequest(http.MethodPost, "/external-grant", strings.NewReader(`{"commandId":"grant-allowed","targetUserId":"318705991371722752","resourceId":11,"quantity":1,"durationMs":0,"reason":"manual"}`))
	allowedRequest.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(allowed, allowedRequest)
	if allowed.Code != http.StatusCreated || len(wallet.grantRequests) != 1 || wallet.grantRequests[0].GetResourceId() != 11 || wallet.grantRequests[0].GetGrantSource() != "manager_center" {
		t.Fatalf("manager grantable resource failed: status=%d body=%s grants=%+v", allowed.Code, allowed.Body.String(), wallet.grantRequests)
	}
}

func TestMainAdminResourceGrantKeepsAdminSource(t *testing.T) {
	wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
		11: {AppCode: "lalu", ResourceId: 11, ResourceType: "badge", Status: "active", Grantable: true},
	}}
	handler := New(wallet, nil, nil, time.Second, nil)
	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu"))
		c.Set(middleware.ContextRequestID, "admin-grant-source-test")
		c.Set(middleware.ContextUserID, uint(7))
		c.Next()
	})
	router.POST("/admin-grant", handler.GrantResource)
	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/admin-grant", strings.NewReader(`{"commandId":"grant-admin","targetUserId":"318705991371722752","resourceId":11,"quantity":1,"reason":"manual"}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)
	if recorder.Code != http.StatusCreated || len(wallet.grantRequests) != 1 || wallet.grantRequests[0].GetGrantSource() != "admin" {
		t.Fatalf("main admin grant source changed: status=%d request=%+v body=%s", recorder.Code, wallet.grantRequests, recorder.Body.String())
	}
}

func TestExternalResourceListUsesManagerCatalogAndHidesVIPTrialCard(t *testing.T) {
	wallet := &mockResourceWallet{listedResources: []*walletv1.Resource{
		{AppCode: "lalu", ResourceId: 11, ResourceType: "badge", Status: "active", Grantable: true, ManagerGrantEnabled: true},
		{AppCode: "lalu", ResourceId: 99, ResourceType: resourceTypeVIPTrialCard, Status: "active", Grantable: true, ManagerGrantEnabled: true},
	}}
	handler := New(wallet, nil, nil, time.Second, nil)
	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu"))
		c.Set(middleware.ContextRequestID, "external-resource-list-test")
		c.Next()
	})
	router.GET("/external-resources", handler.ListExternalGrantResources)
	recorder := httptest.NewRecorder()
	router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/external-resources?page=1&page_size=20", nil))
	if recorder.Code != http.StatusOK || strings.Contains(recorder.Body.String(), `"id":99`) {
		t.Fatalf("external list status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if len(wallet.listResourceRequests) != 1 || !wallet.listResourceRequests[0].GetActiveOnly() || !wallet.listResourceRequests[0].GetManagerGrantOnly() {
		t.Fatalf("external list did not use manager catalog: %+v", wallet.listResourceRequests)
	}
}

func TestGenericResourceGroupGrantAllowsVIPTrialCardItem(t *testing.T) {
	wallet := &mockResourceWallet{resourceGroups: map[int64]*walletv1.ResourceGroup{
		88: {
			AppCode: "lalu",
			GroupId: 88,
			Items: []*walletv1.ResourceGroupItem{{
				ResourceId: 99,
				Resource:   &walletv1.Resource{ResourceId: 99, ResourceType: resourceTypeVIPTrialCard},
			}},
		},
	}}
	router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/admin/resource-grants/group", strings.NewReader(`{"commandId":"grant-group-1","targetUserId":"318705991371722752","groupId":88,"reason":"manual"}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusCreated || len(wallet.grantGroupRequests) != 1 || wallet.grantGroupRequests[0].GetGroupId() != 88 {
		t.Fatalf("generic group grant must forward vip trial card group to wallet owner: status=%d body=%s requests=%+v", recorder.Code, recorder.Body.String(), wallet.grantGroupRequests)
	}
}

func TestVIPGrantPermissionCanLookupResourceGrantTarget(t *testing.T) {
	db, sqlMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sql mock failed: %v", err)
	}
	defer db.Close()
	sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*LIMIT 1`).
		WillReturnRows(sqlmock.NewRows([]string{"user_id", "current_display_user_id", "username", "avatar"}).AddRow(int64(318705991371722752), "111", "target", "avatar.png"))

	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu"))
		c.Set(middleware.ContextRequestID, "vip-target-test")
		c.Set(middleware.ContextPermissions, []string{"vip-config:grant"})
		c.Next()
	})
	RegisterRoutes(router.Group(""), New(&mockResourceWallet{}, nil, db, time.Second, nil))
	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/admin/resource-grants/target?keyword=111", nil)
	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"userId":"318705991371722752"`) {
		t.Fatalf("vip grant permission should resolve target: status=%d body=%s", recorder.Code, recorder.Body.String())
	}
	if err := sqlMock.ExpectationsWereMet(); err != nil {
		t.Fatalf("lookup sql mismatch: %v", err)
	}
}

func TestListResourceGrantsResolvesDisplayIDBeforeWalletFilter(t *testing.T) {
	db, sqlMock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sql mock failed: %v", err)
	}
	defer db.Close()

	wallet := &mockResourceWallet{}
	router := newResourceHandlerTestRouter(New(wallet, nil, db, time.Second, nil))
	resolvedUserID := int64(327702592329093120)
	sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*LIMIT 1`).
		WithArgs(
			"lalu",
			"111",
			"111",
			"111",
			sqlmock.AnyArg(),
			"111",
			"%111%",
			"111",
			"111",
			"111",
			sqlmock.AnyArg(),
			"111",
		).
		WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(resolvedUserID))

	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodGet, "/admin/resource-grants?target_user_id=111", nil)
	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("list grants status mismatch: %d %s", recorder.Code, recorder.Body.String())
	}
	if len(wallet.listGrantRequests) != 1 {
		t.Fatalf("expected one wallet list request, got %d", len(wallet.listGrantRequests))
	}
	if got := wallet.listGrantRequests[0].GetTargetUserId(); got != resolvedUserID {
		t.Fatalf("target display id should be resolved before wallet filter: got %d want %d", got, resolvedUserID)
	}
	if err := sqlMock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func TestRevokeUserResourceTargetsOneEntitlement(t *testing.T) {
	wallet := &mockResourceWallet{}
	router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
	recorder := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/admin/users/318705991371722752/resources/ent-host-badge/revoke", strings.NewReader(`{"reason":"remove host badge"}`))
	request.Header.Set("Content-Type", "application/json")
	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("revoke user resource status mismatch: %d %s", recorder.Code, recorder.Body.String())
	}
	if len(wallet.revokeUserResourceRequests) != 1 {
		t.Fatalf("expected one precise revoke request, got %d", len(wallet.revokeUserResourceRequests))
	}
	req := wallet.revokeUserResourceRequests[0]
	if req.GetUserId() != 318705991371722752 || req.GetEntitlementId() != "ent-host-badge" || req.GetReason() != "remove host badge" || req.GetOperatorUserId() != 7 {
		t.Fatalf("precise revoke request mismatch: %+v", req)
	}
	if !strings.Contains(recorder.Body.String(), `"entitlementId":"ent-host-badge"`) || !strings.Contains(recorder.Body.String(), `"status":"revoked"`) {
		t.Fatalf("precise revoke response mismatch: %s", recorder.Body.String())
	}
}

func newResourceHandlerTestRouter(handler *Handler) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(func(c *gin.Context) {
		c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu"))
		c.Set(middleware.ContextRequestID, "resource-handler-test")
		c.Set(middleware.ContextUserID, uint(7))
		c.Set(middleware.ContextUsername, "tester")
		// 测试路由走真实 RegisterRoutes，权限一次性放开，避免中间件短路导致无法覆盖路由优先级。
		c.Set(middleware.ContextPermissions, []string{
			"emoji-pack:create",
			"emoji-pack:view",
			"gift:delete",
			"gift:view",
			"gift:update",
			"gift:create",
			"gift:status",
			"resource:create",
			"resource:delete",
			"resource-grant:create",
			"resource-grant:revoke",
			"resource-grant:view",
			"resource-group:create",
			"resource-group:update",
			"resource-group:view",
			"resource-shop:update",
			"resource-shop:view",
			"resource:update",
			"resource:view",
		})
		c.Next()
	})
	RegisterRoutes(router.Group(""), handler)
	return router
}

type mockResourceWallet struct {
	walletclient.Client
	resources                  map[int64]*walletv1.Resource
	resourceGroups             map[int64]*walletv1.ResourceGroup
	updates                    []*walletv1.UpdateResourceRequest
	deletedResources           []*walletv1.DeleteResourceRequest
	batchDeletedResources      []*walletv1.BatchDeleteResourcesRequest
	batchCreatedGifts          []*walletv1.BatchCreateGiftConfigsRequest
	deletedGifts               []*walletv1.DeleteGiftConfigRequest
	listGrantRequests          []*walletv1.ListResourceGrantsRequest
	listResourceRequests       []*walletv1.ListResourcesRequest
	listedResources            []*walletv1.Resource
	grantRequests              []*walletv1.GrantResourceRequest
	grantGroupRequests         []*walletv1.GrantResourceGroupRequest
	revokeUserResourceRequests []*walletv1.RevokeUserResourceRequest
}

func (m *mockResourceWallet) ListResources(_ context.Context, req *walletv1.ListResourcesRequest) (*walletv1.ListResourcesResponse, error) {
	m.listResourceRequests = append(m.listResourceRequests, req)
	return &walletv1.ListResourcesResponse{Resources: m.listedResources, Total: int64(len(m.listedResources))}, nil
}

func (m *mockResourceWallet) GrantResource(_ context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error) {
	m.grantRequests = append(m.grantRequests, req)
	return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{
		AppCode: req.GetAppCode(), GrantId: "grant-result", CommandId: req.GetCommandId(), TargetUserId: req.GetTargetUserId(),
		GrantSource: req.GetGrantSource(), GrantSubjectType: "resource", GrantSubjectId: fmt.Sprint(req.GetResourceId()), Status: "succeeded",
	}}, nil
}

func (m *mockResourceWallet) GrantResourceGroup(_ context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
	m.grantGroupRequests = append(m.grantGroupRequests, req)
	return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{
		AppCode: req.GetAppCode(), GrantId: "group-grant-result", CommandId: req.GetCommandId(), TargetUserId: req.GetTargetUserId(),
		GrantSource: req.GetGrantSource(), GrantSubjectType: "resource_group", GrantSubjectId: fmt.Sprint(req.GetGroupId()), Status: "succeeded",
	}}, nil
}

func (m *mockResourceWallet) RevokeUserResource(_ context.Context, req *walletv1.RevokeUserResourceRequest) (*walletv1.RevokeUserResourceResponse, error) {
	m.revokeUserResourceRequests = append(m.revokeUserResourceRequests, req)
	return &walletv1.RevokeUserResourceResponse{Resource: &walletv1.UserResourceEntitlement{
		EntitlementId:     req.GetEntitlementId(),
		UserId:            req.GetUserId(),
		ResourceId:        91,
		Status:            "revoked",
		RemainingQuantity: 0,
		Equipped:          false,
	}}, nil
}

func (m *mockResourceWallet) GetResource(ctx context.Context, req *walletv1.GetResourceRequest) (*walletv1.GetResourceResponse, error) {
	resource := m.resources[req.GetResourceId()]
	if resource == nil {
		return nil, fmt.Errorf("resource not found")
	}
	return &walletv1.GetResourceResponse{Resource: resource}, nil
}

func (m *mockResourceWallet) GetResourceGroup(_ context.Context, req *walletv1.GetResourceGroupRequest) (*walletv1.GetResourceGroupResponse, error) {
	group := m.resourceGroups[req.GetGroupId()]
	if group == nil {
		return nil, fmt.Errorf("resource group not found")
	}
	return &walletv1.GetResourceGroupResponse{Group: group}, nil
}

func (m *mockResourceWallet) UpdateResource(ctx context.Context, req *walletv1.UpdateResourceRequest) (*walletv1.ResourceResponse, error) {
	m.updates = append(m.updates, req)
	return &walletv1.ResourceResponse{Resource: &walletv1.Resource{
		AppCode:             req.GetAppCode(),
		ResourceId:          req.GetResourceId(),
		ResourceCode:        req.GetResourceCode(),
		ResourceType:        req.GetResourceType(),
		Name:                req.GetName(),
		Status:              req.GetStatus(),
		Grantable:           req.GetGrantable(),
		GrantStrategy:       req.GetGrantStrategy(),
		WalletAssetType:     req.GetWalletAssetType(),
		WalletAssetAmount:   req.GetWalletAssetAmount(),
		UsageScopes:         req.GetUsageScopes(),
		AssetUrl:            req.GetAssetUrl(),
		PreviewUrl:          req.GetPreviewUrl(),
		AnimationUrl:        req.GetAnimationUrl(),
		MetadataJson:        req.GetMetadataJson(),
		SortOrder:           req.GetSortOrder(),
		ManagerGrantEnabled: req.GetManagerGrantEnabled(),
		PriceType:           req.GetPriceType(),
		CoinPrice:           req.GetCoinPrice(),
	}}, nil
}

func (m *mockResourceWallet) DeleteResource(ctx context.Context, req *walletv1.DeleteResourceRequest) (*walletv1.ResourceResponse, error) {
	m.deletedResources = append(m.deletedResources, req)
	resource := m.resources[req.GetResourceId()]
	if resource == nil {
		return nil, fmt.Errorf("resource not found")
	}
	return &walletv1.ResourceResponse{Resource: resource}, nil
}

func (m *mockResourceWallet) BatchDeleteResources(ctx context.Context, req *walletv1.BatchDeleteResourcesRequest) (*walletv1.BatchDeleteResourcesResponse, error) {
	m.batchDeletedResources = append(m.batchDeletedResources, req)
	resp := &walletv1.BatchDeleteResourcesResponse{Resources: make([]*walletv1.Resource, 0, len(req.GetResourceIds()))}
	for _, resourceID := range req.GetResourceIds() {
		resource := m.resources[resourceID]
		if resource == nil {
			return nil, fmt.Errorf("resource not found")
		}
		resp.Resources = append(resp.Resources, resource)
	}
	return resp, nil
}

func (m *mockResourceWallet) BatchCreateGiftConfigs(ctx context.Context, req *walletv1.BatchCreateGiftConfigsRequest) (*walletv1.BatchCreateGiftConfigsResponse, error) {
	m.batchCreatedGifts = append(m.batchCreatedGifts, req)
	resp := &walletv1.BatchCreateGiftConfigsResponse{Gifts: make([]*walletv1.GiftConfig, 0, len(req.GetItems()))}
	for _, item := range req.GetItems() {
		resp.Gifts = append(resp.Gifts, &walletv1.GiftConfig{
			AppCode:    item.GetAppCode(),
			GiftId:     item.GetGiftId(),
			ResourceId: item.GetResourceId(),
			Status:     item.GetStatus(),
			Name:       item.GetName(),
			VipLevel:   item.GetVipLevel(),
		})
	}
	return resp, nil
}

func (m *mockResourceWallet) DeleteGiftConfig(ctx context.Context, req *walletv1.DeleteGiftConfigRequest) (*walletv1.GiftConfigResponse, error) {
	m.deletedGifts = append(m.deletedGifts, req)
	return &walletv1.GiftConfigResponse{Gift: &walletv1.GiftConfig{
		AppCode:    req.GetAppCode(),
		GiftId:     req.GetGiftId(),
		ResourceId: 11,
		Status:     "active",
		Name:       "Rose",
	}}, nil
}

func (m *mockResourceWallet) ListResourceGrants(ctx context.Context, req *walletv1.ListResourceGrantsRequest) (*walletv1.ListResourceGrantsResponse, error) {
	// 列表过滤的修正点发生在 admin-server 调 wallet 之前；测试只需要捕获请求形状，
	// 返回空页即可避免后续用户资料补全查询干扰断言。
	m.listGrantRequests = append(m.listGrantRequests, req)
	return &walletv1.ListResourceGrantsResponse{Grants: []*walletv1.ResourceGrant{}, Total: 0}, nil
}
