package hostorg

import (
	"context"
	"encoding/json"
	"reflect"
	"strconv"
	"strings"
	"testing"

	"hyapp-admin-server/internal/integration/userclient"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type fakeHostOrgUserClient struct {
	userclient.Client
	users    map[int64]*userclient.User
	displays map[string]*userclient.UserIdentity
	calls    []string
}

func (f *fakeHostOrgUserClient) GetUser(_ context.Context, req userclient.GetUserRequest) (*userclient.User, error) {
	f.calls = append(f.calls, "get:"+strconv.FormatInt(req.UserID, 10))
	if user := f.users[req.UserID]; user != nil {
		return user, nil
	}
	return nil, status.Error(codes.NotFound, "user not found")
}

func (f *fakeHostOrgUserClient) ResolveDisplayUserID(_ context.Context, req userclient.ResolveDisplayUserIDRequest) (*userclient.UserIdentity, error) {
	f.calls = append(f.calls, "resolve:"+req.DisplayUserID)
	if identity := f.displays[req.DisplayUserID]; identity != nil {
		return identity, nil
	}
	return nil, status.Error(codes.NotFound, "display_user_id not found")
}

func TestResolveDisplayUserIDFallsBackToNumericUserID(t *testing.T) {
	client := &fakeHostOrgUserClient{
		users: map[int64]*userclient.User{
			123456: {UserID: 123456, DisplayUserID: "9999", DefaultDisplayUserID: "123456"},
		},
		displays: map[string]*userclient.UserIdentity{},
	}
	service := NewService(client, nil, nil)

	userID, err := service.resolveDisplayUserID(context.Background(), "req-held-default", 123456)
	if err != nil {
		t.Fatalf("resolveDisplayUserID returned error: %v", err)
	}
	if userID != 123456 {
		t.Fatalf("fallback user id mismatch: got %d", userID)
	}
	if want := []string{"resolve:123456", "get:123456"}; !reflect.DeepEqual(client.calls, want) {
		t.Fatalf("calls mismatch: got %v want %v", client.calls, want)
	}
}

func TestResolveDisplayUserIDKeepsActiveDisplayPriority(t *testing.T) {
	client := &fakeHostOrgUserClient{
		users: map[int64]*userclient.User{
			123456: {UserID: 123456, DisplayUserID: "9999", DefaultDisplayUserID: "123456"},
		},
		displays: map[string]*userclient.UserIdentity{
			"123456": {UserID: 88, DisplayUserID: "123456"},
		},
	}
	service := NewService(client, nil, nil)

	userID, err := service.resolveDisplayUserID(context.Background(), "req-active-display", 123456)
	if err != nil {
		t.Fatalf("resolveDisplayUserID returned error: %v", err)
	}
	if userID != 88 {
		t.Fatalf("active display user id mismatch: got %d", userID)
	}
	if want := []string{"resolve:123456"}; !reflect.DeepEqual(client.calls, want) {
		t.Fatalf("calls mismatch: got %v want %v", client.calls, want)
	}
}

// TestNormalizeListQueryAllowsComputedSorts 锁定后台列表支持的计算排序字段不会在查询归一化时被清掉。
func TestNormalizeListQueryAllowsComputedSorts(t *testing.T) {
	for _, sortBy := range []string{sortByMerchantBalance, sortBySalaryWallet, sortByTotalRechargeUSDT} {
		query := normalizeListQuery(listQuery{Page: 1, PageSize: 50, SortBy: sortBy, SortDirection: "asc"})
		if query.SortBy != sortBy || query.SortDirection != "asc" {
			t.Fatalf("expected sort %s asc to be kept, got sort_by=%q direction=%q", sortBy, query.SortBy, query.SortDirection)
		}
	}
}

// TestManagerListItemJSONHasNoAgencyOrParentBD 锁定后台经理列表的响应口径：经理是 BD Leader/Admin 的上级，不暴露 Agency 或上级 BD 字段。
func TestManagerListItemJSONHasNoAgencyOrParentBD(t *testing.T) {
	item := ManagerListItem{
		UserID:          316033326332776448,
		DisplayUserID:   "165549",
		Username:        "Kitty",
		Status:          "active",
		Contact:         "+63",
		CanGrantVehicle: true,
		CanGrantVIP:     true,
		CanBlockUser:    false,
		BDLeaderCount:   3,
		LastInvitedAtMs: 1720000000000,
	}
	payload, err := json.Marshal(item)
	if err != nil {
		t.Fatalf("marshal manager list item: %v", err)
	}
	body := string(payload)
	for _, want := range []string{
		`"userId":"316033326332776448"`,
		`"displayUserId":"165549"`,
		`"contact":"+63"`,
		`"canGrantVehicle":true`,
		`"canGrantVip":true`,
		`"canBlockUser":false`,
		`"bdLeaderCount":3`,
		`"lastInvitedAtMs":1720000000000`,
	} {
		if !strings.Contains(body, want) {
			t.Fatalf("manager json should contain %s, got %s", want, body)
		}
	}
	if strings.Contains(body, "parentBd") || strings.Contains(body, "ParentBD") || strings.Contains(body, "agency") || strings.Contains(body, "Agency") {
		t.Fatalf("manager json must not expose agency or parent bd fields: %s", body)
	}
}

func TestDecorateManagerScopePolicyUsesPolicyMetadataWithoutAppBranches(t *testing.T) {
	items := []*ManagerListItem{{CanManageCrossRegion: true}}
	decorateManagerScopePolicy(items, &userclient.RoleScopePolicy{
		BaseScope:                   "country",
		ExpandedScope:               "region",
		RegionExpansionConfigurable: true,
	})
	if items[0].ManagerOperationEffectiveScope != "region" || !items[0].ManagerCrossRegionConfigurable || !items[0].LegacyBDLeaderInRegion {
		t.Fatalf("configurable policy decoration mismatch: %+v", items[0])
	}

	items[0].CanManageCrossRegion = true
	decorateManagerScopePolicy(items, &userclient.RoleScopePolicy{
		BaseScope:                   "global",
		ExpandedScope:               "global",
		RegionExpansionConfigurable: false,
	})
	if items[0].ManagerOperationEffectiveScope != "global" || items[0].ManagerCrossRegionConfigurable {
		t.Fatalf("global policy decoration mismatch: %+v", items[0])
	}
	if got := normalizedManagerCrossRegionPermission(pointerBool(true), false); got == nil || *got {
		t.Fatalf("non-configurable app must persist region expansion as false")
	}
}

func pointerBool(value bool) *bool {
	return &value
}

// TestBDRoleFromStatusPathUsesLeaderTableWithAPIPrefix 锁定线上真实 FullPath 带 /api/v1 前缀时，BD Leader 状态仍然写独立负责人表。
func TestBDRoleFromStatusPathUsesLeaderTableWithAPIPrefix(t *testing.T) {
	role, target := bdRoleFromStatusPath("/api/v1/admin/bd-leaders/:user_id/status")
	if role != "bd_leader" || target != "bd_leader_profiles" {
		t.Fatalf("leader status route must use bd_leader_profiles, got role=%q target=%q", role, target)
	}

	role, target = bdRoleFromStatusPath("/api/v1/admin/bds/:user_id/status")
	if role != "bd" || target != "bd_profiles" {
		t.Fatalf("bd status route must use bd_profiles, got role=%q target=%q", role, target)
	}
}

// TestUserCountryRegionSQLUsesCountryMapping 锁定组织列表区域展示口径：从用户国家映射 active 区域，不从身份表区域快照取值。
func TestUserCountryRegionSQLUsesCountryMapping(t *testing.T) {
	joinSQL := userCountryRegionJoin("owner", "owner_country_region", "owner_region")
	for _, want := range []string{
		"LEFT JOIN region_countries owner_country_region",
		"owner_country_region.country_code = owner.country",
		"owner_country_region.status = 'active'",
		"LEFT JOIN regions owner_region",
		"owner_region.region_id = owner_country_region.region_id",
		"owner_region.status = 'active'",
	} {
		if !strings.Contains(joinSQL, want) {
			t.Fatalf("country region join missing %q in %s", want, joinSQL)
		}
	}
	if strings.Contains(joinSQL, "owner.region_id") {
		t.Fatalf("country region join must not read identity or user region snapshots: %s", joinSQL)
	}
	if got := userCountryRegionIDSQL("owner_region"); got != "COALESCE(owner_region.region_id, 0)" {
		t.Fatalf("region id projection mismatch: %s", got)
	}
	if got := userCountryRegionNameSQL("owner_region"); got != "COALESCE(owner_region.name, '')" {
		t.Fatalf("region name projection mismatch: %s", got)
	}
}

// TestSalaryWalletAssetTypeByRole 锁定四个组织身份读取各自工资钱包资产；BD Leader 复用 Admin 工资资产，不能误读普通 BD 资产。
func TestSalaryWalletAssetTypeByRole(t *testing.T) {
	cases := map[string]string{
		"host":      assetHostSalaryUSD,
		"agency":    assetAgencySalaryUSD,
		"bd":        assetBDSalaryUSD,
		"bd_leader": assetAdminSalaryUSD,
		"admin":     assetAdminSalaryUSD,
	}
	for role, want := range cases {
		if got := salaryWalletAssetType(role); got != want {
			t.Fatalf("role %s salary wallet asset mismatch: got %q want %q", role, got, want)
		}
	}
}

// TestNormalizeSalaryWalletHistoryQueryCapsPageSize 避免历史抽屉一次拉太多钱包分录，同时兼容 admin 作为 BD Leader 的旧口径。
func TestNormalizeSalaryWalletHistoryQueryCapsPageSize(t *testing.T) {
	query := normalizeSalaryWalletHistoryQuery(salaryWalletHistoryQuery{Page: 0, PageSize: 500, Role: "admin", UserID: 7})
	if query.Page != 1 || query.PageSize != 100 || query.Role != salaryWalletRoleBDLeader || query.UserID != 7 {
		t.Fatalf("salary wallet history query normalized incorrectly: %+v", query)
	}
}

// TestSortCoinSellerListItemsComputedFields 验证 wallet 聚合字段排序只改变展示顺序，不改动币商身份数据。
func TestSortCoinSellerListItemsComputedFields(t *testing.T) {
	items := []*CoinSellerListItem{
		{UserID: 1, MerchantBalance: 100, TotalRechargeUSDTMicro: 30_000_000},
		{UserID: 2, MerchantBalance: 300, TotalRechargeUSDTMicro: 10_000_000},
		{UserID: 3, MerchantBalance: 200, TotalRechargeUSDTMicro: 20_000_000},
	}

	sortCoinSellerListItems(items, sortByMerchantBalance, "desc")
	if got := []int64{items[0].UserID, items[1].UserID, items[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 {
		t.Fatalf("merchant balance desc order mismatch: %v", got)
	}

	sortCoinSellerListItems(items, sortByTotalRechargeUSDT, "asc")
	if got := []int64{items[0].UserID, items[1].UserID, items[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 {
		t.Fatalf("total recharge usdt asc order mismatch: %v", got)
	}
}

// TestSortSalaryWalletComputedFields 覆盖四个组织列表的工资钱包余额排序；这些余额来自 wallet-service，必须在分页前排序。
func TestSortSalaryWalletComputedFields(t *testing.T) {
	bds := []*userclient.BDProfile{
		{UserID: 1, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 100}},
		{UserID: 2, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 300}},
		{UserID: 3, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 200}},
	}
	sortBDProfilesBySalaryWallet(bds, "desc")
	if got := []int64{bds[0].UserID, bds[1].UserID, bds[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 {
		t.Fatalf("bd salary wallet desc order mismatch: %v", got)
	}
	if page := paginateBDProfiles(bds, 2, 2); len(page) != 1 || page[0].UserID != 1 {
		t.Fatalf("bd salary wallet pagination mismatch: %+v", page)
	}

	agencies := []*userclient.Agency{
		{AgencyID: 10, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 100}},
		{AgencyID: 20, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 300}},
		{AgencyID: 30, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 200}},
	}
	sortAgenciesBySalaryWallet(agencies, "asc")
	if got := []int64{agencies[0].AgencyID, agencies[1].AgencyID, agencies[2].AgencyID}; got[0] != 10 || got[1] != 30 || got[2] != 20 {
		t.Fatalf("agency salary wallet asc order mismatch: %v", got)
	}
	if page := paginateAgencies(agencies, 2, 2); len(page) != 1 || page[0].AgencyID != 20 {
		t.Fatalf("agency salary wallet pagination mismatch: %+v", page)
	}

	hosts := []*userclient.HostProfile{
		{UserID: 1, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 100}},
		{UserID: 2, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 300}},
		{UserID: 3, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 200}},
	}
	sortHostProfilesBySalaryWallet(hosts, "desc")
	if got := []int64{hosts[0].UserID, hosts[1].UserID, hosts[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 {
		t.Fatalf("host salary wallet desc order mismatch: %v", got)
	}
	if page := paginateHostProfiles(hosts, 2, 2); len(page) != 1 || page[0].UserID != 1 {
		t.Fatalf("host salary wallet pagination mismatch: %+v", page)
	}
}
