package http

import (
	"bytes"
	"context"
	"encoding/json"
	"hyapp/services/gateway-service/internal/transport/http/httpkit"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	activityv1 "hyapp.local/api/proto/activity/v1"
	userv1 "hyapp.local/api/proto/user/v1"
	walletv1 "hyapp.local/api/proto/wallet/v1"
	"hyapp/services/gateway-service/internal/auth"
)

func newManagerCenterTestRouter(walletClient *fakeWalletClient, hostClient *fakeUserHostClient, profileClient *fakeUserProfileClient) http.Handler {
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
	handler.SetWalletClient(walletClient)
	handler.SetUserHostClient(hostClient)
	return handler.Routes(auth.NewVerifier("secret"))
}

func newManagerCenterTestRouterWithHostAdmin(walletClient *fakeWalletClient, hostClient *fakeUserHostClient, hostAdminClient *fakeUserHostAdminClient, profileClient *fakeUserProfileClient) http.Handler {
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
	handler.SetWalletClient(walletClient)
	handler.SetUserHostClient(hostClient)
	handler.SetUserHostAdminClient(hostAdminClient)
	return handler.Routes(auth.NewVerifier("secret"))
}

func newManagerCenterTestRouterWithGrowth(walletClient *fakeWalletClient, hostClient *fakeUserHostClient, profileClient *fakeUserProfileClient, growthClient *fakeGrowthLevelClient) http.Handler {
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
	handler.SetWalletClient(walletClient)
	handler.SetUserHostClient(hostClient)
	handler.SetGrowthLevelClient(growthClient)
	return handler.Routes(auth.NewVerifier("secret"))
}

func TestListManagerGrantResourcesChecksCapabilityAndFiltersWalletRequest(t *testing.T) {
	walletClient := &fakeWalletClient{
		listResourcesResp: &walletv1.ListResourcesResponse{
			Total: 1,
			Resources: []*walletv1.Resource{
				{
					ResourceId:        101,
					ResourceType:      "badge",
					Name:              "VIP Badge",
					AssetUrl:          "https://cdn.example/badge.png",
					PreviewUrl:        "https://cdn.example/badge-preview.png",
					WalletAssetType:   "",
					WalletAssetAmount: 0,
					SortOrder:         7,
				},
			},
		},
	}
	hostClient := &fakeUserHostClient{}
	router := newManagerCenterTestRouter(walletClient, hostClient, &fakeUserProfileClient{})
	request := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/resource-grants/resources?resource_type=badge&page=2&page_size=99", nil)
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-resources")
	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 hostClient.lastCapability == nil || hostClient.lastCapability.GetActorUserId() != 42 || hostClient.lastCapability.GetCapability() != "manager_grant_badge" {
		t.Fatalf("manager capability request mismatch: %+v", hostClient.lastCapability)
	}
	if walletClient.lastListResources == nil {
		t.Fatal("wallet ListResources was not called")
	}
	generatedRequestID := assertGeneratedRequestID(t, recorder, "req-manager-resources")
	if req := walletClient.lastListResources; req.GetRequestId() != generatedRequestID || req.GetAppCode() != "lalu" || req.GetResourceType() != "badge" || req.GetPage() != 2 || req.GetPageSize() != 20 || !req.GetActiveOnly() || !req.GetManagerGrantOnly() {
		t.Fatalf("wallet ListResources request mismatch: %+v", req)
	}

	var envelope struct {
		Code      string `json:"code"`
		RequestID string `json:"request_id"`
		Data      struct {
			Items []struct {
				ResourceID string `json:"resource_id"`
				Name       string `json:"name"`
			} `json:"items"`
			Total    int64 `json:"total"`
			Page     int32 `json:"page"`
			PageSize int32 `json:"page_size"`
		} `json:"data"`
	}
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	if envelope.Code != httpkit.CodeOK || envelope.RequestID != generatedRequestID || envelope.Data.Total != 1 || len(envelope.Data.Items) != 1 {
		t.Fatalf("response envelope mismatch: %+v", envelope)
	}
	if envelope.Data.Items[0].ResourceID != "101" || envelope.Data.Items[0].Name != "VIP Badge" {
		t.Fatalf("resource data mismatch: %+v", envelope.Data.Items[0])
	}
}

func TestListManagerGrantResourcesRejectsUnauthorizedManager(t *testing.T) {
	walletClient := &fakeWalletClient{}
	hostClient := &fakeUserHostClient{capabilityResp: &userv1.CheckBusinessCapabilityResponse{Allowed: false, Reason: "active_agency_owner_required"}}
	router := newManagerCenterTestRouter(walletClient, hostClient, &fakeUserProfileClient{})
	request := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/resource-grants/resources", nil)
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-denied")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	assertEnvelope(t, recorder, http.StatusForbidden, httpkit.CodePermissionDenied, "req-manager-denied")
	if walletClient.lastListResources != nil {
		t.Fatalf("unauthorized manager must not reach wallet-service: %+v", walletClient.lastListResources)
	}
}

func TestBusinessUserLookupForwardsSceneAndActor(t *testing.T) {
	profileClient := &fakeUserProfileClient{
		businessLookupResp: &userv1.BusinessUserLookupResponse{
			Users: []*userv1.BusinessUserLookupItem{
				{UserId: 900001, DisplayUserId: "123456", Username: "Alice", Avatar: "https://cdn.example/a.png", Status: userv1.UserStatus_USER_STATUS_ACTIVE, RegionId: 1001, RegionCode: "SA", RegionName: "Saudi Arabia"},
			},
		},
	}
	router := newManagerCenterTestRouter(&fakeWalletClient{}, &fakeUserHostClient{}, profileClient)
	request := httptest.NewRequest(http.MethodGet, "/api/v1/business/users/lookup?scene=manager_resource_grant&keyword=123456&page_size=99", nil)
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-business-lookup")
	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 req := profileClient.lastBusinessLookup; req == nil || req.GetMeta().GetRequestId() != assertGeneratedRequestID(t, recorder, "req-business-lookup") || req.GetActorUserId() != 42 || req.GetScene() != "manager_resource_grant" || req.GetKeyword() != "123456" || req.GetPageSize() != 20 {
		t.Fatalf("business lookup request mismatch: %+v", req)
	}
	var envelope struct {
		Code string `json:"code"`
		Data struct {
			Items []struct {
				UserID string `json:"user_id"`
				Status string `json:"status"`
			} `json:"items"`
			Total int `json:"total"`
		} `json:"data"`
	}
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	if envelope.Code != httpkit.CodeOK || envelope.Data.Total != 1 || envelope.Data.Items[0].UserID != "900001" || envelope.Data.Items[0].Status != "active" {
		t.Fatalf("business lookup response mismatch: %+v", envelope)
	}
}

func TestSearchManagerUsersDefaultsSameCountryForEveryAction(t *testing.T) {
	profileClient := &fakeUserProfileClient{
		businessLookupResp: &userv1.BusinessUserLookupResponse{
			Users: []*userv1.BusinessUserLookupItem{
				{UserId: 900001, DisplayUserId: "163006", Status: userv1.UserStatus_USER_STATUS_ACTIVE},
				{UserId: 900002, DisplayUserId: "163007", Status: userv1.UserStatus_USER_STATUS_ACTIVE},
			},
		},
		usersByID: map[int64]*userv1.User{
			42:     {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US"},
			900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, DisplayUserId: "163006", Country: "US"},
			900002: {UserId: 900002, Status: userv1.UserStatus_USER_STATUS_ACTIVE, DisplayUserId: "163007", Country: "CA"},
		},
	}
	hostClient := &fakeUserHostClient{}
	walletClient := &fakeWalletClient{
		myVipResp: &walletv1.GetMyVipResponse{Vip: &walletv1.UserVip{
			UserId:      900001,
			Level:       7,
			Name:        "VIP7",
			Active:      true,
			StartedAtMs: 1000,
			ExpiresAtMs: 9000,
			UpdatedAtMs: 2000,
		}},
	}
	router := newManagerCenterTestRouter(walletClient, hostClient, profileClient)

	sameCountryReq := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=163&page_size=20", nil)
	sameCountryReq.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	sameCountryReq.Header.Set("X-Request-ID", "req-manager-search-same")
	sameCountryRecorder := httptest.NewRecorder()
	router.ServeHTTP(sameCountryRecorder, sameCountryReq)

	if sameCountryRecorder.Code != http.StatusOK {
		t.Fatalf("same-country search status mismatch: got %d body=%s", sameCountryRecorder.Code, sameCountryRecorder.Body.String())
	}
	var sameEnvelope struct {
		Data struct {
			Items []struct {
				UserID string `json:"user_id"`
			} `json:"items"`
			Total int `json:"total"`
		} `json:"data"`
	}
	if err := json.NewDecoder(sameCountryRecorder.Body).Decode(&sameEnvelope); err != nil {
		t.Fatalf("decode same-country response failed: %v", err)
	}
	if sameEnvelope.Data.Total != 1 || sameEnvelope.Data.Items[0].UserID != "900001" {
		t.Fatalf("same-country search must filter cross-country users: %+v", sameEnvelope.Data)
	}
	if req := profileClient.lastBusinessLookup; req == nil || req.GetScene() != "manager_resource_grant" {
		t.Fatalf("default manager search should use resource grant scene, got %+v", req)
	}
	if walletClient.lastMyVip != nil {
		t.Fatalf("default manager search must not query VIP status: %+v", walletClient.lastMyVip)
	}

	vipReq := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=163&page_size=20&action=send-vip", nil)
	vipReq.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	vipReq.Header.Set("X-Request-ID", "req-manager-search-vip")
	vipRecorder := httptest.NewRecorder()
	router.ServeHTTP(vipRecorder, vipReq)

	if vipRecorder.Code != http.StatusOK {
		t.Fatalf("vip search status mismatch: got %d body=%s", vipRecorder.Code, vipRecorder.Body.String())
	}
	if req := profileClient.lastBusinessLookup; req == nil || req.GetScene() != "manager_vip_grant" {
		t.Fatalf("VIP manager search should use vip grant scene, got %+v", req)
	}
	var vipEnvelope struct {
		Data struct {
			Items []struct {
				CurrentVIP struct {
					Level       int32  `json:"level"`
					Name        string `json:"name"`
					Active      bool   `json:"active"`
					StartedAtMS int64  `json:"started_at_ms"`
					ExpiresAtMS int64  `json:"expires_at_ms"`
				} `json:"current_vip"`
			} `json:"items"`
		} `json:"data"`
	}
	if err := json.NewDecoder(vipRecorder.Body).Decode(&vipEnvelope); err != nil {
		t.Fatalf("decode vip response failed: %v", err)
	}
	if walletClient.lastMyVip == nil || walletClient.lastMyVip.GetUserId() != 900001 {
		t.Fatalf("VIP manager search should query visible target VIP status: %+v", walletClient.lastMyVip)
	}
	if len(vipEnvelope.Data.Items) != 1 || vipEnvelope.Data.Items[0].CurrentVIP.Level != 7 || !vipEnvelope.Data.Items[0].CurrentVIP.Active || vipEnvelope.Data.Items[0].CurrentVIP.ExpiresAtMS != 9000 {
		t.Fatalf("VIP manager search current_vip mismatch: %+v", vipEnvelope.Data.Items)
	}

	for _, item := range []struct {
		action string
		scene  string
	}{
		{action: "block-user", scene: "manager_block_user"},
		{action: "update-user-level", scene: "manager_update_user_level"},
		{action: "add-bd-leader", scene: "manager_add_bd_leader"},
		{action: "add-admin", scene: "manager_add_admin"},
		{action: "add-superadmin", scene: "manager_add_superadmin"},
		{action: "transfer-user-country", scene: "manager_transfer_user_country"},
	} {
		req := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=163&page_size=20&action="+item.action, nil)
		req.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
		req.Header.Set("X-Request-ID", "req-manager-search-"+item.action)
		recorder := httptest.NewRecorder()
		router.ServeHTTP(recorder, req)
		if recorder.Code != http.StatusOK {
			t.Fatalf("%s search status mismatch: got %d body=%s", item.action, recorder.Code, recorder.Body.String())
		}
		if got := profileClient.lastBusinessLookup.GetScene(); got != item.scene {
			t.Fatalf("%s manager search scene = %q, want %q", item.action, got, item.scene)
		}
	}

	allReq := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=163&page_size=20&scope=all&action=transfer-user-country", nil)
	allReq.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	allReq.Header.Set("X-Request-ID", "req-manager-search-all")
	allRecorder := httptest.NewRecorder()
	router.ServeHTTP(allRecorder, allReq)

	if allRecorder.Code != http.StatusOK {
		t.Fatalf("all-country search status mismatch: got %d body=%s", allRecorder.Code, allRecorder.Body.String())
	}
	var allEnvelope struct {
		Data struct {
			Items []struct {
				UserID string `json:"user_id"`
			} `json:"items"`
			Total int `json:"total"`
		} `json:"data"`
	}
	if err := json.NewDecoder(allRecorder.Body).Decode(&allEnvelope); err != nil {
		t.Fatalf("decode all-country response failed: %v", err)
	}
	if allEnvelope.Data.Total != 1 || allEnvelope.Data.Items[0].UserID != "900001" {
		t.Fatalf("scope=all must not bypass manager data scope: %+v", allEnvelope.Data)
	}
	if hostClient.sawCapability("manager_transfer_user_country") {
		t.Fatalf("HTTP scope must not become a second authorization path, saw %#v", hostClient.capabilities)
	}
}

func TestSearchManagerUsersUsesResolvedGlobalPolicyForAllHuwaaActions(t *testing.T) {
	profileClient := &fakeUserProfileClient{
		businessLookupResp: &userv1.BusinessUserLookupResponse{Users: []*userv1.BusinessUserLookupItem{
			{UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
			{UserId: 900002, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
		}},
		usersByID: map[int64]*userv1.User{
			42:     {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US"},
			900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US"},
			900002: {UserId: 900002, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA"},
		},
	}
	hostClient := &fakeUserHostClient{roleScopePolicyResp: &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
		Scene: "manager_operations", BaseScope: "global", ExpandedScope: "global", RegionExpansionConfigurable: false,
	}}}
	router := newManagerCenterTestRouter(&fakeWalletClient{}, hostClient, profileClient)

	bdLeaderRequest := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=900&action=add-bd-leader", nil)
	bdLeaderRequest.Header.Set("Authorization", "Bearer "+signGatewayTokenWithAppCode(t, "secret", 42, "huwaa"))
	bdLeaderRequest.Header.Set("X-Request-ID", "req-huwaa-manager-search-bdleader")
	bdLeaderRecorder := httptest.NewRecorder()
	router.ServeHTTP(bdLeaderRecorder, bdLeaderRequest)

	if bdLeaderRecorder.Code != http.StatusOK {
		t.Fatalf("Huwaa BD Leader search status mismatch: got %d body=%s", bdLeaderRecorder.Code, bdLeaderRecorder.Body.String())
	}
	var bdLeaderEnvelope struct {
		Data struct {
			Items []struct {
				UserID string `json:"user_id"`
			} `json:"items"`
			Total int `json:"total"`
		} `json:"data"`
	}
	if err := json.NewDecoder(bdLeaderRecorder.Body).Decode(&bdLeaderEnvelope); err != nil {
		t.Fatalf("decode Huwaa BD Leader search response failed: %v", err)
	}
	if bdLeaderEnvelope.Data.Total != 2 || profileClient.lastBusinessLookup.GetScene() != "manager_add_bd_leader" || profileClient.lastBusinessLookup.GetMeta().GetAppCode() != "huwaa" {
		t.Fatalf("Huwaa BD Leader search must keep app scope while including cross-country targets: data=%+v req=%+v", bdLeaderEnvelope.Data, profileClient.lastBusinessLookup)
	}

	adminRequest := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=900&action=add-admin", nil)
	adminRequest.Header.Set("Authorization", "Bearer "+signGatewayTokenWithAppCode(t, "secret", 42, "huwaa"))
	adminRequest.Header.Set("X-Request-ID", "req-huwaa-manager-search-admin")
	adminRecorder := httptest.NewRecorder()
	router.ServeHTTP(adminRecorder, adminRequest)
	if adminRecorder.Code != http.StatusOK {
		t.Fatalf("Huwaa admin search status mismatch: got %d body=%s", adminRecorder.Code, adminRecorder.Body.String())
	}
	var adminEnvelope struct {
		Data struct {
			Total int `json:"total"`
		} `json:"data"`
	}
	if err := json.NewDecoder(adminRecorder.Body).Decode(&adminEnvelope); err != nil {
		t.Fatalf("decode Huwaa admin search response failed: %v", err)
	}
	if adminEnvelope.Data.Total != 2 {
		t.Fatalf("Huwaa manager actions must all use global scope: %+v", adminEnvelope.Data)
	}
}

func TestSearchManagerUsersAppliesGeneralCrossRegionScopeToEveryAction(t *testing.T) {
	profileClient := &fakeUserProfileClient{
		businessLookupResp: &userv1.BusinessUserLookupResponse{Users: []*userv1.BusinessUserLookupItem{
			{UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
			{UserId: 900002, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
			{UserId: 900003, Status: userv1.UserStatus_USER_STATUS_ACTIVE},
		}},
		usersByID: map[int64]*userv1.User{
			42:     {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
			900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
			900002: {UserId: 900002, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA", RegionId: 1001},
			900003: {UserId: 900003, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "MX", RegionId: 2002},
		},
	}
	for _, testCase := range []struct {
		name        string
		crossRegion bool
		wantVisible int
	}{
		{name: "closed keeps country", crossRegion: false, wantVisible: 1},
		{name: "open expands inside region", crossRegion: true, wantVisible: 2},
	} {
		t.Run(testCase.name, func(t *testing.T) {
			hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{
				"manager_cross_region": testCase.crossRegion,
			}}
			router := newManagerCenterTestRouter(&fakeWalletClient{}, hostClient, profileClient)
			for _, action := range []string{"send-avatar-frame", "send-vehicle", "send-badge", "send-vip", "update-user-level", "add-bd-leader", "add-admin", "add-superadmin", "block-user", "transfer-user-country"} {
				request := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/users/search?keyword=900&scope=all&action="+action, nil)
				request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
				request.Header.Set("X-Request-ID", "req-manager-cross-region-search")
				recorder := httptest.NewRecorder()
				router.ServeHTTP(recorder, request)
				if recorder.Code != http.StatusOK {
					t.Fatalf("%s status mismatch: got %d body=%s", action, recorder.Code, recorder.Body.String())
				}
				var envelope struct {
					Data struct {
						Total int `json:"total"`
					} `json:"data"`
				}
				if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
					t.Fatalf("decode %s response: %v", action, err)
				}
				if envelope.Data.Total != testCase.wantVisible {
					t.Fatalf("%s visible total = %d, want %d", action, envelope.Data.Total, testCase.wantVisible)
				}
			}
		})
	}
}

func TestGrantManagerResourceUsesServerControlledGrantShape(t *testing.T) {
	walletClient := &fakeWalletClient{}
	hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}
	profileClient := crossRegionManagerProfileClient()
	router := newManagerCenterTestRouter(walletClient, hostClient, profileClient)
	body := []byte(`{"command_id":"mgr-grant-1","target_user_id":"900001","resource_id":"101"}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/resource-grants", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-grant")
	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 hostClient.lastCapability == nil || hostClient.lastCapability.GetActorUserId() != 42 {
		t.Fatalf("manager capability request mismatch: %+v", hostClient.lastCapability)
	}
	if profileClient.lastGet == nil || profileClient.lastGet.GetUserId() != 900001 {
		t.Fatalf("target user lookup mismatch: %+v", profileClient.lastGet)
	}
	if req := walletClient.lastGrantResource; req == nil || req.GetCommandId() != "mgr-grant-1" || req.GetTargetUserId() != 900001 || req.GetResourceId() != 101 || req.GetQuantity() != 1 || req.GetDurationMs() != 604800000 || req.GetOperatorUserId() != 42 || req.GetGrantSource() != "manager_center" || req.GetReason() != "manager_center" {
		t.Fatalf("wallet GrantResource request mismatch: %+v", req)
	}
}

func TestGrantManagerResourceAllowsMultipleResources(t *testing.T) {
	walletClient := &fakeWalletClient{
		resourcesByID: map[int64]*walletv1.Resource{
			202: {ResourceId: 202, ResourceType: "vehicle"},
			203: {ResourceId: 203, ResourceType: "vehicle"},
		},
	}
	hostClient := &fakeUserHostClient{}
	router := newManagerCenterTestRouter(walletClient, hostClient, &fakeUserProfileClient{})
	body := []byte(`{"command_id":"mgr-grant-batch","target_user_id":"900001","resource_ids":["202",203],"duration_ms":7776000000}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/resource-grants", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-grant-batch")
	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(walletClient.grantResourceRequests) != 2 {
		t.Fatalf("wallet grant count mismatch: %+v", walletClient.grantResourceRequests)
	}
	first := walletClient.grantResourceRequests[0]
	second := walletClient.grantResourceRequests[1]
	if first.GetCommandId() != "mgr-grant-batch:resource:202" || first.GetResourceId() != 202 || first.GetQuantity() != 1 || first.GetDurationMs() != 7776000000 {
		t.Fatalf("first batch grant mismatch: %+v", first)
	}
	if second.GetCommandId() != "mgr-grant-batch:resource:203" || second.GetResourceId() != 203 || second.GetQuantity() != 1 || second.GetDurationMs() != 7776000000 {
		t.Fatalf("second batch grant mismatch: %+v", second)
	}
	if !hostClient.sawCapability("manager_grant_vehicle") {
		t.Fatalf("batch vehicle grant must check manager_grant_vehicle, saw %#v", hostClient.capabilities)
	}
	var envelope struct {
		Code string `json:"code"`
		Data struct {
			Total int `json:"total"`
			Items []struct {
				CommandID  string `json:"command_id"`
				ResourceID string `json:"resource_id"`
			} `json:"items"`
		} `json:"data"`
	}
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response: %v", err)
	}
	if envelope.Code != httpkit.CodeOK || envelope.Data.Total != 2 || len(envelope.Data.Items) != 2 || envelope.Data.Items[0].ResourceID != "202" || envelope.Data.Items[1].CommandID != "mgr-grant-batch:resource:203" {
		t.Fatalf("batch response mismatch: %+v", envelope)
	}
}

func TestGrantManagerResourceAllowsConfiguredDuration(t *testing.T) {
	walletClient := &fakeWalletClient{}
	router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{}, &fakeUserProfileClient{})
	body := []byte(`{"command_id":"mgr-grant-30","target_user_id":"900001","resource_id":"101","duration_ms":2592000000}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/resource-grants", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-grant-duration")
	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 req := walletClient.lastGrantResource; req == nil || req.GetDurationMs() != 2592000000 {
		t.Fatalf("manager grant duration was not forwarded: %+v", req)
	}
}

func TestGrantManagerResourceChecksCapabilityByWalletResourceType(t *testing.T) {
	walletClient := &fakeWalletClient{
		resourcesByID: map[int64]*walletv1.Resource{
			202: {ResourceId: 202, ResourceType: "vehicle"},
		},
	}
	hostClient := &fakeUserHostClient{}
	router := newManagerCenterTestRouter(walletClient, hostClient, &fakeUserProfileClient{})
	body := []byte(`{"command_id":"mgr-grant-car","target_user_id":"900001","resource_id":"202"}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/resource-grants", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-grant-car")
	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 walletClient.lastGetResource == nil || walletClient.lastGetResource.GetResourceId() != 202 {
		t.Fatalf("resource lookup mismatch: %+v", walletClient.lastGetResource)
	}
	if hostClient.lastCapability == nil || hostClient.lastCapability.GetCapability() != "manager_cross_region" {
		t.Fatalf("last manager capability should resolve the actor's effective target scope, got %+v", hostClient.lastCapability)
	}
	if !hostClient.sawCapability("manager_center") {
		t.Fatalf("resource grant must still validate the active manager entry permission, saw %#v", hostClient.capabilities)
	}
	if !hostClient.sawCapability("manager_grant_vehicle") {
		t.Fatalf("vehicle grant must check manager_grant_vehicle, saw %#v", hostClient.capabilities)
	}
}

func TestGrantManagerResourceRejectsClientComputedQuantityOrInvalidDuration(t *testing.T) {
	walletClient := &fakeWalletClient{}
	router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{}, &fakeUserProfileClient{})
	cases := []struct {
		name string
		body string
	}{
		{name: "quantity", body: `{"command_id":"mgr-grant-1","target_user_id":"900001","resource_id":"101","quantity":9}`},
		{name: "duration", body: `{"command_id":"mgr-grant-1","target_user_id":"900001","resource_id":"101","duration_ms":86400000}`},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			walletClient.lastGrantResource = nil
			request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/resource-grants", bytes.NewReader([]byte(tc.body)))
			request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
			request.Header.Set("X-Request-ID", "req-manager-grant-shape")
			recorder := httptest.NewRecorder()

			router.ServeHTTP(recorder, request)

			assertEnvelope(t, recorder, http.StatusBadRequest, httpkit.CodeInvalidArgument, "req-manager-grant-shape")
			if walletClient.lastGrantResource != nil {
				t.Fatalf("client computed grant shape must not reach wallet-service: %+v", walletClient.lastGrantResource)
			}
		})
	}
}

func TestGrantManagerResourceRejectsCrossCountryTarget(t *testing.T) {
	walletClient := &fakeWalletClient{}
	profileClient := &fakeUserProfileClient{
		usersByID: map[int64]*userv1.User{
			42:     {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US"},
			900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA"},
		},
	}
	router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{}, profileClient)
	body := []byte(`{"command_id":"mgr-grant-cross","target_user_id":"900001","resource_id":"101"}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/resource-grants", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-cross-country")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	assertEnvelope(t, recorder, http.StatusForbidden, httpkit.CodePermissionDenied, "req-manager-cross-country")
	if walletClient.lastGrantResource != nil {
		t.Fatalf("cross-country target must not reach wallet-service: %+v", walletClient.lastGrantResource)
	}
}

func TestGrantManagerVIPRejectsLevelAboveManagerLimit(t *testing.T) {
	walletClient := &fakeWalletClient{}
	router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{}, &fakeUserProfileClient{})
	body := []byte(`{"command_id":"mgr-vip-6","target_user_id":"900001","level":6}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/vip-grants", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-vip6")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	assertEnvelope(t, recorder, http.StatusBadRequest, httpkit.CodeInvalidArgument, "req-manager-vip6")
	if walletClient.lastGrantVip != nil {
		t.Fatalf("VIP6 must not reach wallet-service: %+v", walletClient.lastGrantVip)
	}
}

func TestGrantManagerVIPRequiresManagerVIPCapability(t *testing.T) {
	walletClient := &fakeWalletClient{}
	hostClient := &fakeUserHostClient{capabilityResp: &userv1.CheckBusinessCapabilityResponse{Allowed: false, Reason: "manager_capability_required"}}
	router := newManagerCenterTestRouter(walletClient, hostClient, &fakeUserProfileClient{})
	body := []byte(`{"command_id":"mgr-vip-denied","target_user_id":"900001","level":5}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/vip-grants", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-vip-denied")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	assertEnvelope(t, recorder, http.StatusForbidden, httpkit.CodePermissionDenied, "req-manager-vip-denied")
	if walletClient.lastGrantVip != nil {
		t.Fatalf("unauthorized VIP grant must not reach wallet-service: %+v", walletClient.lastGrantVip)
	}
	if hostClient.lastCapability == nil || hostClient.lastCapability.GetCapability() != "manager_grant_vip" {
		t.Fatalf("VIP grant should check managerGrantVIPCapability, got %+v", hostClient.lastCapability)
	}
}

func TestGrantManagerVIPUsesManagerCenterGrantSource(t *testing.T) {
	walletClient := &fakeWalletClient{}
	router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}, crossRegionManagerProfileClient())
	body := []byte(`{"command_id":"mgr-vip-5","target_user_id":"900001","level":5}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/vip-grants", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-vip5")
	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 req := walletClient.lastGrantVip; req == nil || req.GetCommandId() != "mgr-vip-5" || req.GetTargetUserId() != 900001 || req.GetLevel() != 5 || req.GetGrantSource() != "manager_center" || req.GetOperatorUserId() != 42 {
		t.Fatalf("wallet GrantVip request mismatch: %+v", req)
	}
}

func TestGrantManagerVIPUsesTrialCardForConfiguredApp(t *testing.T) {
	walletClient := &fakeWalletClient{
		vipProgramResp: &walletv1.GetVipProgramConfigResponse{Config: &walletv1.VipProgramConfig{
			AppCode: "lalu", GrantMode: "trial_card", TrialCardEnabled: true, Status: "active",
		}},
		trialGrantResp: &walletv1.GrantVipTrialCardResponse{
			TrialCard: &walletv1.VipTrialCard{
				TrialCardId: "card-5", EntitlementId: "ent-5", SourceGrantId: "grant-5",
				Level: 5, Status: "active", ExpiresAtMs: 1_800_000_000_000,
			},
			ServerTimeMs: 1_790_000_000_000,
		},
	}
	router := newManagerCenterTestRouter(walletClient, &fakeUserHostClient{}, &fakeUserProfileClient{})
	body := []byte(`{"command_id":"mgr-fami-vip-5","target_user_id":"900001","level":5}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/vip-grants", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-fami-vip5")
	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 walletClient.lastGrantVip != nil {
		t.Fatalf("trial-card program must not call direct GrantVip: %+v", walletClient.lastGrantVip)
	}
	req := walletClient.lastTrialGrant
	if req == nil || req.GetAppCode() != "lalu" || req.GetCommandId() != "mgr-fami-vip-5" || req.GetTargetUserId() != 900001 || req.GetLevel() != 5 || req.GetDurationMs() != 30*24*60*60*1000 || req.GetGrantSource() != "manager_center" || req.GetOperatorUserId() != 42 {
		t.Fatalf("wallet GrantVipTrialCard request mismatch: %+v", req)
	}
	var envelope struct {
		Data struct {
			GrantMode     string `json:"grant_mode"`
			TrialCardID   string `json:"trial_card_id"`
			EntitlementID string `json:"entitlement_id"`
			Active        bool   `json:"active"`
			Equipped      bool   `json:"equipped"`
		} `json:"data"`
	}
	if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	if envelope.Data.GrantMode != "trial_card" || envelope.Data.TrialCardID != "card-5" || envelope.Data.EntitlementID != "ent-5" || envelope.Data.Active || envelope.Data.Equipped {
		t.Fatalf("trial-card manager response mismatch: %+v", envelope.Data)
	}
}

func TestManagerBlockAndUnblockCallUserService(t *testing.T) {
	profileClient := crossRegionManagerProfileClient()
	router := newManagerCenterTestRouter(&fakeWalletClient{}, &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}, profileClient)
	body := []byte(`{"command_id":"block-163006","target_user_id":"900001","blocked_until_ms":1780000000000}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/blocks", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-block")
	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 req := profileClient.lastCreateBlock; req == nil || req.GetCommandId() != "block-163006" || req.GetManagerUserId() != 42 || req.GetTargetUserId() != 900001 || req.GetBlockedUntilMs() != 1780000000000 {
		t.Fatalf("CreateManagerUserBlock request mismatch: %+v", req)
	}

	unblockReq := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/blocks/block-1/unblock", nil)
	unblockReq.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	unblockReq.Header.Set("X-Request-ID", "req-manager-unblock")
	unblockRecorder := httptest.NewRecorder()

	router.ServeHTTP(unblockRecorder, unblockReq)

	if unblockRecorder.Code != http.StatusOK {
		t.Fatalf("unblock status mismatch: got %d body=%s", unblockRecorder.Code, unblockRecorder.Body.String())
	}
	if req := profileClient.lastUnblock; req == nil || req.GetManagerUserId() != 42 || req.GetBlockId() != "block-1" {
		t.Fatalf("UnblockManagerUser request mismatch: %+v", req)
	}
}

func TestSetManagerUserLevelCallsGrowthService(t *testing.T) {
	growthClient := &fakeGrowthLevelClient{}
	router := newManagerCenterTestRouterWithGrowth(&fakeWalletClient{}, &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}, crossRegionManagerProfileClient(), growthClient)
	body := []byte(`{"command_id":"level-wealth-5","target_user_id":"900001","track":"wealth","level":5}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/levels", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-level")
	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 req := growthClient.lastSet; req == nil || req.GetCommandId() != "level-wealth-5" || req.GetUserId() != 900001 || req.GetTrack() != "wealth" || req.GetLevel() != 5 || req.GetOperatorUserId() != 42 {
		t.Fatalf("SetUserLevel request mismatch: %+v", req)
	}
}

func TestCreateManagerBDLeaderCallsHostAdminAfterSameCountryCheck(t *testing.T) {
	hostAdminClient := &fakeUserHostAdminClient{}
	profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
		42:     {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, DisplayUserId: "420001", Username: "manager", Country: "US", RegionId: 1001},
		900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, DisplayUserId: "163006", Username: "target", Country: "US", RegionId: 1001},
	}}
	router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, &fakeUserHostClient{}, hostAdminClient, profileClient)
	body := []byte(`{"command_id":"mgr-bdleader-1","target_user_id":"900001"}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/bd-leaders", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-bdleader")
	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())
	}
	generatedRequestID := assertGeneratedRequestID(t, recorder, "req-manager-bdleader")
	if req := hostAdminClient.lastCreateBDLeader; req == nil || req.GetMeta().GetRequestId() != generatedRequestID || req.GetAdminUserId() != 42 || req.GetTargetUserId() != 900001 || req.GetCommandId() != "mgr-bdleader-1" || req.GetReason() != "manager_center" {
		t.Fatalf("CreateBDLeader request mismatch: %+v", req)
	}

	var envelope struct {
		Code string `json:"code"`
		Data struct {
			BDProfile struct {
				UserID          string `json:"user_id"`
				Role            string `json:"role"`
				CreatedByUserID string `json:"created_by_user_id"`
			} `json:"bd_profile"`
			TargetUser struct {
				UserID        string `json:"user_id"`
				DisplayUserID string `json:"display_user_id"`
			} `json:"target_user"`
		} `json:"data"`
	}
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	if envelope.Code != httpkit.CodeOK || envelope.Data.BDProfile.UserID != "900001" || envelope.Data.BDProfile.Role != "bd_leader" || envelope.Data.BDProfile.CreatedByUserID != "42" || envelope.Data.TargetUser.DisplayUserID != "163006" {
		t.Fatalf("CreateBDLeader response mismatch: %+v", envelope)
	}
}

func TestCreateManagerBDLeaderRejectsCrossCountryTarget(t *testing.T) {
	hostAdminClient := &fakeUserHostAdminClient{}
	profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
		42:     {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
		900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA", RegionId: 1001},
	}}
	hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": false}}
	router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
	body := []byte(`{"command_id":"mgr-bdleader-cross","target_user_id":"900001"}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/bd-leaders", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-bdleader-cross")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	assertEnvelope(t, recorder, http.StatusForbidden, httpkit.CodePermissionDenied, "req-manager-bdleader-cross")
	if hostAdminClient.lastCreateBDLeader != nil {
		t.Fatalf("cross-country target must not reach CreateBDLeader: %+v", hostAdminClient.lastCreateBDLeader)
	}
}

func TestCreateManagerBDLeaderAllowsSameRegionTargetForCrossRegionManager(t *testing.T) {
	hostAdminClient := &fakeUserHostAdminClient{}
	profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
		42:     {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
		900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA", RegionId: 1001},
	}}
	hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}
	router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/bd-leaders", bytes.NewReader([]byte(`{"command_id":"mgr-bdleader-region","target_user_id":"900001"}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-bdleader-region")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("same-region BD Leader status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if req := hostAdminClient.lastCreateBDLeader; req == nil || req.GetTargetUserId() != 900001 {
		t.Fatalf("same-region target must reach CreateBDLeader: %+v", req)
	}
}

func TestCreateManagerBDLeaderRejectsDifferentRegionForCrossRegionManager(t *testing.T) {
	hostAdminClient := &fakeUserHostAdminClient{}
	profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
		42:     {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
		900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "CA", RegionId: 2002},
	}}
	hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}
	router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/bd-leaders", bytes.NewReader([]byte(`{"command_id":"mgr-bdleader-other-region","target_user_id":"900001"}`)))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-bdleader-other-region")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	assertEnvelope(t, recorder, http.StatusForbidden, httpkit.CodePermissionDenied, "req-manager-bdleader-other-region")
	if hostAdminClient.lastCreateBDLeader != nil {
		t.Fatalf("different-region target must not reach CreateBDLeader: %+v", hostAdminClient.lastCreateBDLeader)
	}
}

func TestCreateManagerBDLeaderAllowsCrossCountryTargetForHuwaa(t *testing.T) {
	hostAdminClient := &fakeUserHostAdminClient{}
	profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
		42:     {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
		900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, DisplayUserId: "163006", Country: "CA", RegionId: 2002},
	}}
	hostClient := &fakeUserHostClient{roleScopePolicyResp: &userv1.GetRoleScopePolicyResponse{Policy: &userv1.RoleScopePolicy{
		Scene: "manager_operations", BaseScope: "global", ExpandedScope: "global", RegionExpansionConfigurable: false,
	}}}
	router := newManagerCenterTestRouterWithHostAdmin(&fakeWalletClient{}, hostClient, hostAdminClient, profileClient)
	body := []byte(`{"command_id":"huwaa-mgr-bdleader-cross","target_user_id":"900001"}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/bd-leaders", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayTokenWithAppCode(t, "secret", 42, "huwaa"))
	request.Header.Set("X-Request-ID", "req-huwaa-manager-bdleader-cross")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusOK {
		t.Fatalf("Huwaa cross-country BD Leader status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if req := hostAdminClient.lastCreateBDLeader; req == nil || req.GetMeta().GetAppCode() != "huwaa" || req.GetAdminUserId() != 42 || req.GetTargetUserId() != 900001 {
		t.Fatalf("Huwaa cross-country target must reach app-scoped CreateBDLeader: %+v", req)
	}
}

func TestTransferManagerUserCountryUsesCrossRegionManagerScope(t *testing.T) {
	profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
		42:     {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
		900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "MX", RegionId: 1001},
	}}
	hostClient := &fakeUserHostClient{capabilityAllowed: map[string]bool{"manager_cross_region": true}}
	broadcastClient := &fakeBroadcastClient{}
	handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
	handler.SetWalletClient(&fakeWalletClient{})
	handler.SetUserHostClient(hostClient)
	handler.SetBroadcastClient(broadcastClient)
	router := handler.Routes(auth.NewVerifier("secret"))
	body := []byte(`{"command_id":"mgr-country-1","target_user_id":"900001","country":"CA"}`)
	request := httptest.NewRequest(http.MethodPost, "/api/v1/manager-center/users/country", bytes.NewReader(body))
	request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
	request.Header.Set("X-Request-ID", "req-manager-country")
	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())
	}
	generatedRequestID := assertGeneratedRequestID(t, recorder, "req-manager-country")
	if req := profileClient.lastAdminCountry; req == nil || req.GetMeta().GetRequestId() != generatedRequestID || req.GetAdminUserId() != 42 || req.GetTargetUserId() != 900001 || req.GetCountry() != "CA" || req.GetReason() != "manager_center_country_transfer" {
		t.Fatalf("AdminChangeUserCountry request mismatch: %+v", req)
	}
	if !hostClient.sawCapability("manager_transfer_user_country") {
		t.Fatalf("country transfer must check manager_transfer_user_country, saw %#v", hostClient.capabilities)
	}
	if req := broadcastClient.lastRemove; req == nil || req.GetUserId() != 900001 || req.GetRegionId() != 1001 || req.GetReason() != "manager_user_country_changed" || req.GetMeta().GetAppCode() != "lalu" {
		t.Fatalf("manager country transfer must remove the old region broadcast member: %+v", req)
	}
}

func crossRegionManagerProfileClient() *fakeUserProfileClient {
	return &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
		42:     {UserId: 42, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "US", RegionId: 1001},
		900001: {UserId: 900001, Status: userv1.UserStatus_USER_STATUS_ACTIVE, Country: "MX", RegionId: 1001},
	}}
}

// TestGetManagerOverviewIncludesTeamSalaryDashboard 验证经理概览聚合团队工资卡片：
// user-service 组织树 Agency owner 去重后传给 wallet-service，按本月/上月两个 UTC 周期取回预收入并计算环比。
func TestGetManagerOverviewIncludesTeamSalaryDashboard(t *testing.T) {
	monthStart := time.Date(time.Now().UTC().Year(), time.Now().UTC().Month(), 1, 0, 0, 0, 0, time.UTC)
	thisCycle := monthStart.Format("2006-01")
	lastCycle := monthStart.AddDate(0, -1, 0).Format("2006-01")
	hostClient := &fakeUserHostClient{
		managerTeamResp: &userv1.ListManagerTeamAgenciesResponse{
			Agencies: []*userv1.ManagerTeamAgency{
				{AgencyId: 1, OwnerUserId: 501},
				{AgencyId: 2, OwnerUserId: 502},
				// 同一 owner 拥有多个 Agency 时必须去重，否则 wallet 侧 IN 查询携带重复收款人。
				{AgencyId: 3, OwnerUserId: 501},
			},
			TotalBdLeaders: 2,
			TotalBds:       5,
		},
	}
	walletClient := &fakeWalletClient{
		teamSalaryStatsResp: &walletv1.GetTeamHostSalaryStatsResponse{
			Stats: []*walletv1.TeamHostSalaryCycleStat{
				{CycleKey: thisCycle, EstimatedHostSalaryUsdMinor: 1215200, ActiveHostCount: 128},
				{CycleKey: lastCycle, EstimatedHostSalaryUsdMinor: 926900, ActiveHostCount: 96},
			},
		},
	}
	router := newManagerCenterTestRouter(walletClient, hostClient, &fakeUserProfileClient{})
	request := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/overview", 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 hostClient.lastManagerTeam == nil || hostClient.lastManagerTeam.GetManagerUserId() != 42 {
		t.Fatalf("manager team request mismatch: %+v", hostClient.lastManagerTeam)
	}
	statsReq := walletClient.lastTeamSalaryStats
	if statsReq == nil || statsReq.GetAppCode() != "lalu" || statsReq.GetNowMs() <= 0 {
		t.Fatalf("team salary stats request mismatch: %+v", statsReq)
	}
	if owners := statsReq.GetAgencyOwnerUserIds(); len(owners) != 2 || owners[0] != 501 || owners[1] != 502 {
		t.Fatalf("agency owner ids must be deduped: %v", owners)
	}
	if cycles := statsReq.GetCycleKeys(); len(cycles) != 2 || cycles[0] != thisCycle || cycles[1] != lastCycle {
		t.Fatalf("cycle keys mismatch: %v", cycles)
	}

	var envelope struct {
		Data struct {
			Dashboard struct {
				MonthLabel           string  `json:"month_label"`
				ThisMonthSalary      float64 `json:"this_month_salary"`
				LastMonthSalary      float64 `json:"last_month_salary"`
				ActiveHosts          int32   `json:"active_hosts"`
				ActiveHostsLastMonth int32   `json:"active_hosts_last_month"`
				Growth               string  `json:"growth"`
				TeamBDLeaders        int32   `json:"team_bd_leaders"`
				TeamBDs              int32   `json:"team_bds"`
				TeamAgencies         int32   `json:"team_agencies"`
			} `json:"dashboard"`
		} `json:"data"`
	}
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	dashboard := envelope.Data.Dashboard
	if dashboard.MonthLabel != monthStart.Format("Jan 2006") {
		t.Fatalf("month label mismatch: %+v", dashboard)
	}
	if dashboard.ThisMonthSalary != 12152 || dashboard.LastMonthSalary != 9269 {
		t.Fatalf("salary amounts mismatch: %+v", dashboard)
	}
	if dashboard.ActiveHosts != 128 || dashboard.ActiveHostsLastMonth != 96 {
		t.Fatalf("active hosts mismatch: %+v", dashboard)
	}
	// (12152 - 9269) / 9269 = 31.1% → 四舍五入 +31%。
	if dashboard.Growth != "+31%" {
		t.Fatalf("growth mismatch: %+v", dashboard)
	}
	if dashboard.TeamBDLeaders != 2 || dashboard.TeamBDs != 5 || dashboard.TeamAgencies != 3 {
		t.Fatalf("team structure mismatch: %+v", dashboard)
	}
}

// TestGetManagerOverviewDegradesWhenTeamStatsUnavailable 验证统计链路故障时经理概览仍返回身份与权限，不返回 dashboard。
func TestGetManagerOverviewDegradesWhenTeamStatsUnavailable(t *testing.T) {
	hostClient := &fakeUserHostClient{managerTeamErr: context.DeadlineExceeded}
	walletClient := &fakeWalletClient{}
	router := newManagerCenterTestRouter(walletClient, hostClient, &fakeUserProfileClient{})
	request := httptest.NewRequest(http.MethodGet, "/api/v1/manager-center/overview", 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())
	}
	var envelope struct {
		Data map[string]json.RawMessage `json:"data"`
	}
	if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	if _, ok := envelope.Data["dashboard"]; ok {
		t.Fatal("dashboard must be omitted when team stats aggregation fails")
	}
	if _, ok := envelope.Data["permissions"]; !ok {
		t.Fatal("permissions must still be returned when team stats aggregation fails")
	}
	if walletClient.lastTeamSalaryStats != nil {
		t.Fatal("wallet stats must not be called when team resolution fails")
	}
}

type fakeGrowthLevelClient struct {
	lastSet *activityv1.SetUserLevelRequest
}

func (f *fakeGrowthLevelClient) GetMyLevelOverview(context.Context, *activityv1.GetMyLevelOverviewRequest) (*activityv1.GetMyLevelOverviewResponse, error) {
	return &activityv1.GetMyLevelOverviewResponse{}, nil
}

func (f *fakeGrowthLevelClient) GetLevelTrack(context.Context, *activityv1.GetLevelTrackRequest) (*activityv1.GetLevelTrackResponse, error) {
	return &activityv1.GetLevelTrackResponse{}, nil
}

func (f *fakeGrowthLevelClient) BatchGetUserLevelDisplayProfiles(context.Context, *activityv1.BatchGetUserLevelDisplayProfilesRequest) (*activityv1.BatchGetUserLevelDisplayProfilesResponse, error) {
	return &activityv1.BatchGetUserLevelDisplayProfilesResponse{}, nil
}

func (f *fakeGrowthLevelClient) ListLevelRewards(context.Context, *activityv1.ListLevelRewardsRequest) (*activityv1.ListLevelRewardsResponse, error) {
	return &activityv1.ListLevelRewardsResponse{}, nil
}

func (f *fakeGrowthLevelClient) SetUserLevel(_ context.Context, req *activityv1.SetUserLevelRequest) (*activityv1.SetUserLevelResponse, error) {
	f.lastSet = req
	return &activityv1.SetUserLevelResponse{EventId: "event-1", Status: "applied", Track: req.GetTrack(), NewLevel: req.GetLevel(), TotalValue: 1000}, nil
}
