package payment

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"slices"
	"testing"

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

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/gin-gonic/gin"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func TestMoneyScopeReadPermissionsIncludeWithdrawalWorkspaces(t *testing.T) {
	t.Parallel()
	for _, permission := range []string{
		financeViewPermission,
		"finance-withdrawal:view",
		"finance-withdrawal:audit",
		"operations-withdrawal:view",
		"operations-withdrawal:audit",
	} {
		if !slices.Contains(moneyScopeReadPermissions, permission) {
			t.Fatalf("money scope read permission missing %s", permission)
		}
	}
}

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

	wallet := &mockPaymentWallet{rechargeBillsResp: &walletv1.ListRechargeBillsResponse{}}
	router := newPaymentHandlerTestRouter(New(wallet, db, nil, nil, nil))
	userID := int64(327702592329093120)
	sellerUserID := int64(328453424662192128)
	expectUserDisplayIDLookup(sqlMock, "111", userID)
	expectUserDisplayIDLookup(sqlMock, "167864", sellerUserID)

	request := httptest.NewRequest(http.MethodGet, "/admin/payment/recharge-bills?display_user_id=111&seller_display_user_id=167864", nil)
	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 wallet.lastRechargeBills == nil ||
		wallet.lastRechargeBills.GetUserId() != userID ||
		wallet.lastRechargeBills.GetSellerUserId() != sellerUserID {
		t.Fatalf("recharge bill user filters should be resolved before wallet call: %+v", wallet.lastRechargeBills)
	}
	if err := sqlMock.ExpectationsWereMet(); err != nil {
		t.Fatalf("sql expectations mismatch: %v", err)
	}
}

func TestListRechargeBillsReturnsProviderAmountMinor(t *testing.T) {
	wallet := &mockPaymentWallet{rechargeBillsResp: &walletv1.ListRechargeBillsResponse{
		Bills: []*walletv1.RechargeBill{{
			AppCode:             "lalu",
			TransactionId:       "tx-h5",
			RechargeType:        "mifapay_recharge",
			Status:              "succeeded",
			CurrencyCode:        "SAR",
			CoinAmount:          120000,
			UsdMinorAmount:      1500,
			ProviderAmountMinor: 5625,
			CreatedAtMs:         1760000000000,
		}},
		Total: 1,
	}}
	router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
	request := httptest.NewRequest(http.MethodGet, "/admin/payment/recharge-bills?status=succeeded", nil)
	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 response adminPaymentTestResponse
	if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	items := response.Data["items"].([]any)
	item := items[0].(map[string]any)
	if item["providerAmountMinor"] != float64(5625) || item["currencyCode"] != "SAR" {
		t.Fatalf("provider amount should be returned for finance recharge detail: %+v", item)
	}
}

func TestGetRechargeBillSummaryKeepsOrdinaryThirdParty(t *testing.T) {
	wallet := &mockPaymentWallet{rechargeSummaryResp: &walletv1.GetRechargeBillSummaryResponse{
		Total:      &walletv1.RechargeBillSummaryBucket{BillCount: 31, CoinAmount: 310000, UsdMinorAmount: 12345},
		ThirdParty: &walletv1.RechargeBillSummaryBucket{BillCount: 31, CoinAmount: 310000, UsdMinorAmount: 12345},
	}}
	router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
	request := httptest.NewRequest(http.MethodGet, "/admin/payment/recharge-bills/summary?recharge_type=third_party&status=succeeded", nil)
	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 response adminPaymentTestResponse
	if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	thirdParty := response.Data["thirdParty"].(map[string]any)
	total := response.Data["total"].(map[string]any)
	if thirdParty["billCount"] != float64(31) || thirdParty["usdMinorAmount"] != float64(12345) {
		t.Fatalf("ordinary third party summary should be preserved: %+v", response.Data)
	}
	if total["billCount"] != float64(31) || total["usdMinorAmount"] != float64(12345) {
		t.Fatalf("total should be recomputed from ordinary third party bucket: %+v", response.Data)
	}
}

func TestFinanceRechargeOverviewReusesFinanceChannelsAndDropsWalletStockCoinSeller(t *testing.T) {
	wallet := &mockPaymentWallet{}
	wallet.rechargeOverviewFn = func(req *walletv1.GetRechargeBillOverviewRequest) (*walletv1.GetRechargeBillOverviewResponse, error) {
		regionID := req.GetRegionId()
		googleUSD, thirdPartyUSD := int64(100), int64(200)
		if regionID == 7 {
			googleUSD, thirdPartyUSD = 40, 60
		}
		switch req.GetRechargeType() {
		case "google_play_recharge":
			return &walletv1.GetRechargeBillOverviewResponse{Regions: []*walletv1.RechargeBillRegionBucket{{RegionId: 7, UsdMinorAmount: googleUSD}}}, nil
		case "third_party":
			return &walletv1.GetRechargeBillOverviewResponse{Regions: []*walletv1.RechargeBillRegionBucket{{RegionId: 7, UsdMinorAmount: thirdPartyUSD}}}, nil
		default:
			return &walletv1.GetRechargeBillOverviewResponse{Daily: []*walletv1.RechargeBillDailyBucket{{
				Date: "2026-07-10", GoogleUsdMinor: googleUSD, ThirdPartyUsdMinor: thirdPartyUSD,
				// wallet overview 的旧库存币商列必须被 Finance 订单/H5 聚合清零后重填。
				CoinSellerUsdMinor: 9999,
			}}}, nil
		}
	}
	handler := New(wallet, nil, nil, nil, nil)

	overview, err := handler.FinanceRechargeOverview(context.Background(), FinanceRechargeOverviewQuery{
		AppCode: "lalu", StartAtMS: 100, EndAtMS: 200, IncludeRegions: true, TZOffsetMinutes: 480,
	})
	if err != nil {
		t.Fatalf("FinanceRechargeOverview failed: %v", err)
	}
	if len(overview.Daily) != 1 || overview.Daily[0].GoogleUSDMinor != 100 || overview.Daily[0].ThirdPartyUSDMinor != 200 || overview.Daily[0].CoinSellerUSDMinor != 0 {
		t.Fatalf("global Finance channels mismatch: %#v", overview.Daily)
	}
	if len(overview.Regions) != 1 || overview.Regions[0].RegionID != 7 || overview.Regions[0].TotalUSDMinor != 300 || overview.Regions[0].DailyAvailable || len(overview.Regions[0].Daily) != 0 {
		t.Fatalf("global region Finance total mismatch: %#v", overview.Regions)
	}
	if len(wallet.rechargeOverviewCalls) != 3 {
		t.Fatalf("default overview must not fan out by region, got %d calls: %#v", len(wallet.rechargeOverviewCalls), wallet.rechargeOverviewCalls)
	}

	scoped, err := handler.FinanceRechargeOverview(context.Background(), FinanceRechargeOverviewQuery{
		AppCode: "lalu", StartAtMS: 100, EndAtMS: 200, ScopeRegionIDs: []int64{7}, IncludeRegions: true, TZOffsetMinutes: 480,
	})
	if err != nil {
		t.Fatalf("scoped FinanceRechargeOverview failed: %v", err)
	}
	if len(scoped.Daily) != 1 || scoped.Daily[0].GoogleUSDMinor != 40 || scoped.Daily[0].ThirdPartyUSDMinor != 60 {
		t.Fatalf("scoped global channels mismatch: %#v", scoped.Daily)
	}
	if len(scoped.Regions) != 1 || !scoped.Regions[0].DailyAvailable || scoped.Regions[0].TotalUSDMinor != 100 || len(scoped.Regions[0].Daily) != 1 {
		t.Fatalf("explicit region scope must include daily channels: %#v", scoped.Regions)
	}
	if len(wallet.rechargeOverviewCalls) != 4 {
		t.Fatalf("explicit one-region scope should add only one Finance aggregation, got %d calls: %#v", len(wallet.rechargeOverviewCalls), wallet.rechargeOverviewCalls)
	}
}

func TestListThirdPartyPaymentChannelsForwardsIncludeDisabledMethods(t *testing.T) {
	wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{
		Channels: []*walletv1.ThirdPartyPaymentChannel{{
			AppCode:      "lalu",
			ProviderCode: "mifapay",
			ProviderName: "MiFaPay",
			Status:       "active",
			SortOrder:    10,
			Methods: []*walletv1.ThirdPartyPaymentMethod{{
				MethodId:          810,
				AppCode:           "lalu",
				ProviderCode:      "mifapay",
				ProviderName:      "MiFaPay",
				CountryCode:       "SA",
				CountryName:       "Saudi Arabia",
				CurrencyCode:      "SAR",
				PayWay:            "Card",
				PayType:           "MADA",
				MethodName:        "MADA",
				Status:            "active",
				UsdToCurrencyRate: "1.00000000",
			}},
		}, {
			AppCode:      "lalu",
			ProviderCode: "v5pay",
			ProviderName: "V5Pay",
			Status:       "active",
			SortOrder:    20,
			Methods: []*walletv1.ThirdPartyPaymentMethod{{
				MethodId:          2310,
				AppCode:           "lalu",
				ProviderCode:      "v5pay",
				ProviderName:      "V5Pay",
				CountryCode:       "SA",
				CountryName:       "Saudi Arabia",
				CurrencyCode:      "SAR",
				PayWay:            "Ewallet",
				PayType:           "STCPAY",
				MethodName:        "STC Pay",
				Status:            "active",
				UsdToCurrencyRate: "1.00000000",
			}},
		}},
	}}
	router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
	request := httptest.NewRequest(http.MethodGet, "/admin/payment/third-party-channels?status=active", nil)
	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 wallet.lastThirdPartyChannels == nil ||
		wallet.lastThirdPartyChannels.GetAppCode() != "lalu" ||
		wallet.lastThirdPartyChannels.GetProviderCode() != "" ||
		wallet.lastThirdPartyChannels.GetStatus() != "active" ||
		!wallet.lastThirdPartyChannels.GetIncludeDisabledMethods() {
		t.Fatalf("third party channels request mismatch: %+v", wallet.lastThirdPartyChannels)
	}
	var response adminPaymentTestResponse
	if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	items := response.Data["items"].([]any)
	channel := items[0].(map[string]any)
	v5Channel := items[1].(map[string]any)
	methods := channel["methods"].([]any)
	v5Methods := v5Channel["methods"].([]any)
	if response.Code != 0 || channel["providerCode"] != "mifapay" || v5Channel["providerCode"] != "v5pay" || len(methods) != 1 || len(v5Methods) != 1 || methods[0].(map[string]any)["payType"] != "MADA" || v5Methods[0].(map[string]any)["payType"] != "STCPAY" {
		t.Fatalf("third party channels response mismatch: %+v", response)
	}
}

func TestListTemporaryPaymentLinksForwardsFilters(t *testing.T) {
	wallet := &mockPaymentWallet{temporaryOrdersResp: &walletv1.ListTemporaryRechargeOrdersResponse{
		Total: 1,
		Orders: []*walletv1.ExternalRechargeOrder{{
			OrderId:             "tmp_1001",
			AppCode:             "lalu",
			AudienceType:        "temporary",
			ProductName:         "Temporary payment USD 1.99",
			CoinAmount:          0,
			UsdMinorAmount:      199,
			ProviderCode:        "mifapay",
			PaymentMethodId:     810,
			CountryCode:         "SA",
			CurrencyCode:        "SAR",
			ProviderAmountMinor: 746,
			PayWay:              "Card",
			PayType:             "MADA",
			PayUrl:              "https://pay.example/tmp_1001",
			ProviderOrderId:     "mifa_1001",
			Status:              "redirected",
			CreatedAtMs:         1700000000000,
			UpdatedAtMs:         1700000000001,
		}},
	}}
	router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
	request := httptest.NewRequest(http.MethodGet, "/admin/payment/temporary-links?status=redirected&provider_code=mifapay&keyword=tmp&page=2&page_size=30", nil)
	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 wallet.lastTemporaryOrders == nil ||
		wallet.lastTemporaryOrders.GetAppCode() != "lalu" ||
		wallet.lastTemporaryOrders.GetStatus() != "redirected" ||
		wallet.lastTemporaryOrders.GetProviderCode() != "mifapay" ||
		wallet.lastTemporaryOrders.GetKeyword() != "tmp" ||
		wallet.lastTemporaryOrders.GetPage() != 2 ||
		wallet.lastTemporaryOrders.GetPageSize() != 30 {
		t.Fatalf("temporary link list request mismatch: %+v", wallet.lastTemporaryOrders)
	}
	var response adminPaymentTestResponse
	if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	items := response.Data["items"].([]any)
	item := items[0].(map[string]any)
	if response.Code != 0 || response.Data["total"].(float64) != 1 || item["orderId"] != "tmp_1001" || item["payUrl"] != "https://pay.example/tmp_1001" || item["status"] != "redirected" {
		t.Fatalf("temporary link response mismatch: %+v", response)
	}
}

func TestGetTemporaryPaymentLinkRefreshesSingleOrder(t *testing.T) {
	wallet := &mockPaymentWallet{temporaryOrderResp: &walletv1.H5RechargeOrderResponse{Order: &walletv1.ExternalRechargeOrder{
		OrderId:      "tmp_1001",
		AppCode:      "lalu",
		AudienceType: "temporary",
		ProviderCode: "v5pay",
		PayUrl:       "https://pay.example/tmp_1001",
		Status:       "paid",
	}}}
	router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
	request := httptest.NewRequest(http.MethodGet, "/admin/payment/temporary-links/tmp_1001", nil)
	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 wallet.lastTemporaryOrder == nil ||
		wallet.lastTemporaryOrder.GetAppCode() != "lalu" ||
		wallet.lastTemporaryOrder.GetOrderId() != "tmp_1001" {
		t.Fatalf("temporary link get request mismatch: %+v", wallet.lastTemporaryOrder)
	}
	var response adminPaymentTestResponse
	if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	if response.Code != 0 || response.Data["orderId"] != "tmp_1001" || response.Data["status"] != "paid" {
		t.Fatalf("temporary link get response mismatch: %+v", response)
	}
}

func TestCreateTemporaryPaymentLinkRecordsOwner(t *testing.T) {
	userDB, userSQL, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create user sql mock failed: %v", err)
	}
	defer userDB.Close()
	store, storeSQL, closeStore := newPaymentStoreSQLMock(t)
	defer closeStore()
	expectStoreMoneyAccess(storeSQL, 7, []moneyScopeFixture{{appCode: "lalu", regionID: 2}})
	expectRegionCountry(userSQL, "lalu", 2, "SA", 1)
	expectStoreRecordOwnerAndEnrich(storeSQL, "lalu", "tmp_created", 7, 2)

	wallet := &mockPaymentWallet{thirdPartyChannelsResp: temporaryMethodChannels("lalu", "mifapay", 810, "SA")}
	router := newPaymentHandlerTestRouter(New(wallet, userDB, nil, store, nil))
	request := httptest.NewRequest(http.MethodPost, "/admin/payment/temporary-links", bytes.NewBufferString(`{"appCode":"lalu","regionId":2,"usdMinorAmount":1299,"providerCode":"mifapay","paymentMethodId":810,"returnUrl":"https://h5.example/pay"}`))
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusCreated {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if wallet.lastCreateTemporary == nil ||
		wallet.lastCreateTemporary.GetAppCode() != "lalu" ||
		wallet.lastCreateTemporary.GetUsdMinorAmount() != 1299 ||
		wallet.lastCreateTemporary.GetProviderCode() != "mifapay" ||
		wallet.lastCreateTemporary.GetPaymentMethodId() != 810 ||
		wallet.lastCreateTemporary.GetReturnUrl() != "https://h5.example/pay" ||
		wallet.lastCreateTemporary.GetPayerAccount() != "tester" {
		t.Fatalf("create temporary request mismatch: %+v", wallet.lastCreateTemporary)
	}
	var response adminPaymentTestResponse
	if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	if response.Code != 0 || response.Data["orderId"] != "tmp_created" || response.Data["regionId"].(float64) != 2 || response.Data["ownerUserId"].(float64) != 7 {
		t.Fatalf("create payment link response mismatch: %+v", response)
	}
	if err := userSQL.ExpectationsWereMet(); err != nil {
		t.Fatalf("user sql expectations mismatch: %v", err)
	}
	if err := storeSQL.ExpectationsWereMet(); err != nil {
		t.Fatalf("store sql expectations mismatch: %v", err)
	}
}

func TestCreateTemporaryPaymentLinkRejectsOutOfScopeRegion(t *testing.T) {
	userDB, _, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create user sql mock failed: %v", err)
	}
	defer userDB.Close()
	store, storeSQL, closeStore := newPaymentStoreSQLMock(t)
	defer closeStore()
	expectStoreMoneyAccess(storeSQL, 7, []moneyScopeFixture{{appCode: "lalu", regionID: 3}})

	wallet := &mockPaymentWallet{thirdPartyChannelsResp: temporaryMethodChannels("lalu", "mifapay", 810, "SA")}
	router := newPaymentHandlerTestRouter(New(wallet, userDB, nil, store, nil))
	request := httptest.NewRequest(http.MethodPost, "/admin/payment/temporary-links", bytes.NewBufferString(`{"appCode":"lalu","regionId":2,"usdMinorAmount":1299,"providerCode":"mifapay","paymentMethodId":810}`))
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusForbidden {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if wallet.lastThirdPartyChannels != nil || wallet.lastCreateTemporary != nil {
		t.Fatalf("out-of-scope request must fail before wallet calls: channels=%+v create=%+v", wallet.lastThirdPartyChannels, wallet.lastCreateTemporary)
	}
	if err := storeSQL.ExpectationsWereMet(); err != nil {
		t.Fatalf("store sql expectations mismatch: %v", err)
	}
}

func TestCreateTemporaryPaymentLinkRejectsCountryRegionMismatch(t *testing.T) {
	userDB, userSQL, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create user sql mock failed: %v", err)
	}
	defer userDB.Close()
	store, storeSQL, closeStore := newPaymentStoreSQLMock(t)
	defer closeStore()
	expectStoreMoneyAccess(storeSQL, 7, []moneyScopeFixture{{appCode: "lalu", regionID: 2}})
	expectRegionCountry(userSQL, "lalu", 2, "SA", 0)

	wallet := &mockPaymentWallet{thirdPartyChannelsResp: temporaryMethodChannels("lalu", "mifapay", 810, "SA")}
	router := newPaymentHandlerTestRouter(New(wallet, userDB, nil, store, nil))
	request := httptest.NewRequest(http.MethodPost, "/admin/payment/temporary-links", bytes.NewBufferString(`{"appCode":"lalu","regionId":2,"usdMinorAmount":1299,"providerCode":"mifapay","paymentMethodId":810}`))
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if wallet.lastCreateTemporary != nil {
		t.Fatalf("country mismatch must fail before create order: %+v", wallet.lastCreateTemporary)
	}
	if err := userSQL.ExpectationsWereMet(); err != nil {
		t.Fatalf("user sql expectations mismatch: %v", err)
	}
	if err := storeSQL.ExpectationsWereMet(); err != nil {
		t.Fatalf("store sql expectations mismatch: %v", err)
	}
}

func TestGetMoneyPerformanceAggregatesPaidTemporaryOwners(t *testing.T) {
	walletDB, walletSQL, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create wallet sql mock failed: %v", err)
	}
	defer walletDB.Close()
	store, storeSQL, closeStore := newPaymentStoreSQLMock(t)
	defer closeStore()
	expectStoreMoneyAccess(storeSQL, 7, []moneyScopeFixture{{appCode: "lalu", regionID: 2}})
	walletSQL.ExpectQuery(`(?s)SELECT app_code, order_id, usd_minor_amount.*FROM external_recharge_orders`).
		WithArgs("lalu", "temporary").
		WillReturnRows(sqlmock.NewRows([]string{"app_code", "order_id", "usd_minor_amount", "status", "country_code", "provider_code", "payment_method_id", "pay_url", "created_at_ms", "updated_at_ms"}).
			AddRow("lalu", "tmp_paid", int64(1299), "paid", "SA", "mifapay", int64(810), "https://pay.example/tmp_paid", int64(1700000000000), int64(1700000000100)).
			AddRow("lalu", "tmp_credited", int64(2000), "credited", "SA", "mifapay", int64(810), "https://pay.example/tmp_credited", int64(1700000000000), int64(1700000000200)))
	expectStoreOwners(storeSQL, "lalu", []ownerFixture{{orderID: "tmp_paid", adminUserID: 7, regionID: 2}, {orderID: "tmp_credited", adminUserID: 7, regionID: 2}})
	expectStoreUsers(storeSQL, 7, "tester", "Tester")

	router := newPaymentHandlerTestRouter(New(&mockPaymentWallet{}, nil, walletDB, store, nil))
	request := httptest.NewRequest(http.MethodGet, "/admin/money/performance", nil)
	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 response adminPaymentTestResponse
	if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	items := response.Data["items"].([]any)
	item := items[0].(map[string]any)
	summary := response.Data["summary"].(map[string]any)
	if item["paidLinkCount"].(float64) != 2 || item["paidUsdMinor"].(float64) != 3299 || item["operatorName"] != "Tester" || summary["paidUsdMinor"].(float64) != 3299 {
		t.Fatalf("performance response mismatch: %+v", response)
	}
	if err := walletSQL.ExpectationsWereMet(); err != nil {
		t.Fatalf("wallet sql expectations mismatch: %v", err)
	}
	if err := storeSQL.ExpectationsWereMet(); err != nil {
		t.Fatalf("store sql expectations mismatch: %v", err)
	}
}

func TestSetThirdPartyPaymentMethodStatusForwardsOperator(t *testing.T) {
	wallet := &mockPaymentWallet{}
	router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
	request := httptest.NewRequest(http.MethodPatch, "/admin/payment/third-party-methods/810/status", bytes.NewBufferString(`{"enabled":false}`))
	request.Header.Set("Content-Type", "application/json")
	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 wallet.lastSetMethodStatus == nil ||
		wallet.lastSetMethodStatus.GetMethodId() != 810 ||
		wallet.lastSetMethodStatus.GetEnabled() ||
		wallet.lastSetMethodStatus.GetOperatorUserId() != 7 {
		t.Fatalf("method status request mismatch: %+v", wallet.lastSetMethodStatus)
	}
}

func TestUpdateThirdPartyPaymentRateTrimsRateAndForwardsOperator(t *testing.T) {
	wallet := &mockPaymentWallet{}
	router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
	request := httptest.NewRequest(http.MethodPatch, "/admin/payment/third-party-rates/810", bytes.NewBufferString(`{"usdToCurrencyRate":" 3.75000000 "}`))
	request.Header.Set("Content-Type", "application/json")
	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 wallet.lastUpdateRate == nil ||
		wallet.lastUpdateRate.GetMethodId() != 810 ||
		wallet.lastUpdateRate.GetUsdToCurrencyRate() != "3.75000000" ||
		wallet.lastUpdateRate.GetOperatorUserId() != 7 {
		t.Fatalf("payment rate request mismatch: %+v", wallet.lastUpdateRate)
	}
}

func TestSyncThirdPartyPaymentMethodsDefaultsToV5Pay(t *testing.T) {
	wallet := &mockPaymentWallet{syncMethodsResp: &walletv1.SyncThirdPartyPaymentMethodsResponse{
		ProviderCode:        "v5pay",
		ScannedCountryCount: 11,
		FetchedCountryCount: 8,
		CreatedCount:        6,
		UpdatedCount:        20,
		SkippedCount:        1,
		Issues: []*walletv1.ThirdPartyPaymentMethodSyncIssue{{
			CountryCode:  "SA",
			CurrencyCode: "SAR",
			Code:         "1013",
			Message:      "app is invalid",
		}},
	}}
	router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
	request := httptest.NewRequest(http.MethodPost, "/admin/payment/third-party-methods/sync", bytes.NewBufferString(`{}`))
	request.Header.Set("Content-Type", "application/json")
	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 wallet.lastSyncMethods == nil ||
		wallet.lastSyncMethods.GetAppCode() != "lalu" ||
		wallet.lastSyncMethods.GetProviderCode() != "v5pay" ||
		wallet.lastSyncMethods.GetOperatorUserId() != 7 {
		t.Fatalf("sync methods request mismatch: %+v", wallet.lastSyncMethods)
	}
	var response adminPaymentTestResponse
	if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	if response.Code != 0 ||
		response.Data["providerCode"] != "v5pay" ||
		response.Data["createdCount"].(float64) != 6 ||
		response.Data["failedCountryCount"].(float64) != 1 {
		t.Fatalf("sync methods response mismatch: %+v", response)
	}
}

func TestSyncThirdPartyPaymentRatesAppliesMarkupAndCeilRules(t *testing.T) {
	wallet := &mockPaymentWallet{thirdPartyChannelsResp: &walletv1.ListThirdPartyPaymentChannelsResponse{
		Channels: []*walletv1.ThirdPartyPaymentChannel{{
			AppCode:      "lalu",
			ProviderCode: "mifapay",
			ProviderName: "MiFaPay",
			Status:       "active",
			Methods: []*walletv1.ThirdPartyPaymentMethod{
				{MethodId: 810, CurrencyCode: "SAR", Status: "active"},
				{MethodId: 811, CurrencyCode: "BHD", Status: "disabled"},
			},
		}, {
			AppCode:      "lalu",
			ProviderCode: "v5pay",
			ProviderName: "V5Pay",
			Status:       "active",
			Methods: []*walletv1.ThirdPartyPaymentMethod{
				{MethodId: 2310, CurrencyCode: "SAR", Status: "active"},
				{MethodId: 2311, CurrencyCode: "IDR", Status: "active"},
			},
		}},
	}}
	handler := New(wallet, nil, nil, nil, nil)
	handler.exchangeRates = fakeExchangeRateClient{result: exchangeRateResult{
		Rates:       map[string]string{"SAR": "3.75000000", "BHD": "0.37600000", "IDR": "18510.10000000"},
		SourceNames: []string{"open.er-api.com"},
	}}
	router := newPaymentHandlerTestRouter(handler)
	request := httptest.NewRequest(http.MethodPost, "/admin/payment/third-party-rates/sync", bytes.NewBufferString(`{"markupPercent":3}`))
	request.Header.Set("Content-Type", "application/json")
	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 wallet.lastThirdPartyChannels == nil || !wallet.lastThirdPartyChannels.GetIncludeDisabledMethods() {
		t.Fatalf("sync must read active and disabled methods: %+v", wallet.lastThirdPartyChannels)
	}
	if len(wallet.updateRateRequests) != 4 {
		t.Fatalf("sync should update every method with a fetched currency rate: %+v", wallet.updateRateRequests)
	}
	got := map[int64]string{}
	for _, req := range wallet.updateRateRequests {
		if req.GetOperatorUserId() != 7 {
			t.Fatalf("sync operator mismatch: %+v", req)
		}
		got[req.GetMethodId()] = req.GetUsdToCurrencyRate()
	}
	if got[810] != "3.9" || got[811] != "0.4" || got[2310] != "3.9" || got[2311] != "19066" {
		t.Fatalf("synced rates mismatch: %+v", got)
	}
	var response adminPaymentTestResponse
	if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
		t.Fatalf("decode response failed: %v", err)
	}
	if response.Code != 0 || response.Data["updatedCount"].(float64) != 4 {
		t.Fatalf("sync response mismatch: %+v", response)
	}
}

func TestApplyThirdPartyPaymentRateMarkupCeilsHighRateToInteger(t *testing.T) {
	rate, ok := applyThirdPartyPaymentRateMarkup("18510.10000000", 0)

	if !ok || rate != "18511" {
		t.Fatalf("IDR high rate must ceil to integer: rate=%q ok=%v", rate, ok)
	}
}

func TestSyncThirdPartyPaymentRatesRejectsInvalidMarkupPercent(t *testing.T) {
	wallet := &mockPaymentWallet{}
	router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
	request := httptest.NewRequest(http.MethodPost, "/admin/payment/third-party-rates/sync", bytes.NewBufferString(`{"markupPercent":-1}`))
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusBadRequest {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if wallet.lastThirdPartyChannels != nil {
		t.Fatalf("invalid markup must fail before reading payment methods: %+v", wallet.lastThirdPartyChannels)
	}
}

func TestCreateRechargeProductForwardsWebCoinSellerAudience(t *testing.T) {
	wallet := &mockPaymentWallet{}
	router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
	body := `{"amountUsdt":"10.000000","coinAmount":880000,"productName":"Seller 10 USD","description":"coin seller tier","audienceType":"coin_seller","platform":"web","regionIds":[7100],"enabled":true,"appVisible":false}`
	request := httptest.NewRequest(http.MethodPost, "/admin/payment/recharge-products", bytes.NewBufferString(body))
	request.Header.Set("Content-Type", "application/json")
	recorder := httptest.NewRecorder()

	router.ServeHTTP(recorder, request)

	if recorder.Code != http.StatusCreated {
		t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
	}
	if wallet.lastCreateProduct == nil ||
		wallet.lastCreateProduct.GetAmountMicro() != 10_000_000 ||
		wallet.lastCreateProduct.GetCoinAmount() != 880000 ||
		wallet.lastCreateProduct.GetAudienceType() != "coin_seller" ||
		wallet.lastCreateProduct.GetPlatform() != "web" ||
		len(wallet.lastCreateProduct.GetRegionIds()) != 1 ||
		wallet.lastCreateProduct.GetRegionIds()[0] != 7100 ||
		!wallet.lastCreateProduct.GetEnabled() ||
		wallet.lastCreateProduct.AppVisible == nil ||
		wallet.lastCreateProduct.GetAppVisible() ||
		wallet.lastCreateProduct.GetOperatorUserId() != 7 {
		t.Fatalf("create recharge product request mismatch: %+v", wallet.lastCreateProduct)
	}
}

func newPaymentHandlerTestRouter(handler *Handler) *gin.Engine {
	gin.SetMode(gin.TestMode)
	router := gin.New()
	router.Use(func(c *gin.Context) {
		// 后台 handler 只依赖 app_code、request_id 和操作者；测试在这里模拟鉴权中间件已完成后的上下文。
		c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), "lalu"))
		c.Set(middleware.ContextRequestID, "payment-handler-test")
		c.Set(middleware.ContextUserID, uint(7))
		c.Set(middleware.ContextUsername, "tester")
		c.Next()
	})
	router.GET("/admin/payment/recharge-bills", handler.ListRechargeBills)
	router.GET("/admin/payment/recharge-bills/summary", handler.GetRechargeBillSummary)
	router.GET("/admin/payment/third-party-channels", handler.ListThirdPartyPaymentChannels)
	router.GET("/admin/payment/temporary-links", handler.ListTemporaryPaymentLinks)
	router.POST("/admin/payment/temporary-links", handler.CreateTemporaryPaymentLink)
	router.GET("/admin/payment/temporary-links/:order_id", handler.GetTemporaryPaymentLink)
	router.GET("/admin/money/performance", handler.GetMoneyPerformance)
	router.PATCH("/admin/payment/third-party-methods/:method_id/status", handler.SetThirdPartyPaymentMethodStatus)
	router.POST("/admin/payment/third-party-methods/sync", handler.SyncThirdPartyPaymentMethods)
	router.PATCH("/admin/payment/third-party-rates/:method_id", handler.UpdateThirdPartyPaymentRate)
	router.POST("/admin/payment/third-party-rates/sync", handler.SyncThirdPartyPaymentRates)
	router.POST("/admin/payment/recharge-products", handler.CreateRechargeProduct)
	return router
}

type adminPaymentTestResponse struct {
	Code int            `json:"code"`
	Data map[string]any `json:"data"`
}

func expectUserIdentityLookup(sqlMock sqlmock.Sqlmock, keyword string, userID int64) {
	sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*LIMIT 1`).
		WithArgs(
			"lalu",
			keyword,
			keyword,
			keyword,
			sqlmock.AnyArg(),
			keyword,
			"%"+keyword+"%",
			keyword,
			keyword,
			keyword,
			sqlmock.AnyArg(),
			keyword,
		).
		WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(userID))
}

func expectUserDisplayIDLookup(sqlMock sqlmock.Sqlmock, displayUserID string, userID int64) {
	sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*LIMIT 1`).
		WithArgs(
			"lalu",
			displayUserID,
			displayUserID,
			sqlmock.AnyArg(),
			displayUserID,
		).
		WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(userID))
}

type moneyScopeFixture struct {
	appCode  string
	regionID int64
}

type ownerFixture struct {
	orderID     string
	adminUserID uint
	regionID    int64
}

func newPaymentStoreSQLMock(t *testing.T) (*repository.Store, sqlmock.Sqlmock, func()) {
	t.Helper()
	sqlDB, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create store sql mock failed: %v", err)
	}
	mock.MatchExpectationsInOrder(false)
	gormDB, err := gorm.Open(mysql.New(mysql.Config{Conn: sqlDB, SkipInitializeWithVersion: true}), &gorm.Config{})
	if err != nil {
		_ = sqlDB.Close()
		t.Fatalf("create gorm db failed: %v", err)
	}
	return repository.New(gormDB), mock, func() {
		_ = sqlDB.Close()
	}
}

func expectStoreMoneyAccess(mock sqlmock.Sqlmock, userID uint, scopes []moneyScopeFixture) {
	mock.ExpectQuery("SELECT \\* FROM `admin_users` WHERE `admin_users`.`id` = \\? ORDER BY `admin_users`.`id` LIMIT \\?").
		WillReturnRows(sqlmock.NewRows([]string{"id", "username", "name", "status"}).AddRow(userID, "tester", "Tester", "active"))
	mock.ExpectQuery("SELECT \\* FROM `admin_user_roles` WHERE `admin_user_roles`.`user_id` = \\?").
		WillReturnRows(sqlmock.NewRows([]string{"user_id", "role_id"}))
	rows := sqlmock.NewRows([]string{"id", "user_id", "app_code", "region_id", "created_at_ms", "updated_at_ms"})
	for index, scope := range scopes {
		rows.AddRow(index+1, userID, scope.appCode, scope.regionID, int64(1700000000000), int64(1700000000000))
	}
	mock.ExpectQuery("SELECT \\* FROM `admin_user_money_scopes` WHERE user_id = \\? ORDER BY app_code ASC, region_id ASC").
		WillReturnRows(rows)
}

func expectRegionCountry(mock sqlmock.Sqlmock, appCode string, regionID int64, countryCode string, count int) {
	mock.ExpectQuery(`(?s)SELECT COUNT\(\*\).*FROM region_countries.*WHERE app_code = \? AND region_id = \? AND country_code = \? AND status = 'active'`).
		WithArgs(appCode, regionID, countryCode).
		WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(count))
}

func expectStoreRecordOwnerAndEnrich(mock sqlmock.Sqlmock, appCode string, orderID string, adminUserID uint, regionID int64) {
	mock.ExpectBegin()
	mock.ExpectExec("INSERT INTO `admin_temporary_payment_link_owners`").
		WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()
	expectStoreOwners(mock, appCode, []ownerFixture{{orderID: orderID, adminUserID: adminUserID, regionID: regionID}})
	expectStoreUsers(mock, adminUserID, "tester", "Tester")
}

func expectStoreOwners(mock sqlmock.Sqlmock, appCode string, owners []ownerFixture) {
	rows := sqlmock.NewRows([]string{"app_code", "order_id", "admin_user_id", "region_id", "created_at_ms", "updated_at_ms"})
	for _, owner := range owners {
		rows.AddRow(appCode, owner.orderID, owner.adminUserID, owner.regionID, int64(1700000000000), int64(1700000000000))
	}
	mock.ExpectQuery("SELECT \\* FROM `admin_temporary_payment_link_owners` WHERE app_code = \\? AND order_id IN").
		WillReturnRows(rows)
}

func expectStoreUsers(mock sqlmock.Sqlmock, userID uint, username string, name string) {
	mock.ExpectQuery("SELECT \\* FROM `admin_users` WHERE id IN").
		WillReturnRows(sqlmock.NewRows([]string{"id", "username", "name", "status"}).AddRow(userID, username, name, "active"))
}

func temporaryMethodChannels(appCode string, providerCode string, methodID int64, countryCode string) *walletv1.ListThirdPartyPaymentChannelsResponse {
	return &walletv1.ListThirdPartyPaymentChannelsResponse{Channels: []*walletv1.ThirdPartyPaymentChannel{{
		AppCode:      appCode,
		ProviderCode: providerCode,
		ProviderName: providerCode,
		Status:       "active",
		Methods: []*walletv1.ThirdPartyPaymentMethod{{
			MethodId:     methodID,
			AppCode:      appCode,
			ProviderCode: providerCode,
			ProviderName: providerCode,
			CountryCode:  countryCode,
			CountryName:  countryCode,
			CurrencyCode: countryCode,
			PayWay:       "Card",
			PayType:      "CARD",
			MethodName:   "Card",
			Status:       "active",
		}},
	}}}
}

type mockPaymentWallet struct {
	walletclient.Client

	lastRechargeBills      *walletv1.ListRechargeBillsRequest
	rechargeBillsResp      *walletv1.ListRechargeBillsResponse
	rechargeOverviewFn     func(*walletv1.GetRechargeBillOverviewRequest) (*walletv1.GetRechargeBillOverviewResponse, error)
	rechargeOverviewCalls  []*walletv1.GetRechargeBillOverviewRequest
	rechargeSummaryResp    *walletv1.GetRechargeBillSummaryResponse
	lastThirdPartyChannels *walletv1.ListThirdPartyPaymentChannelsRequest
	thirdPartyChannelsResp *walletv1.ListThirdPartyPaymentChannelsResponse
	lastTemporaryOrders    *walletv1.ListTemporaryRechargeOrdersRequest
	temporaryOrdersResp    *walletv1.ListTemporaryRechargeOrdersResponse
	lastCreateTemporary    *walletv1.CreateTemporaryRechargeOrderRequest
	createTemporaryResp    *walletv1.H5RechargeOrderResponse
	lastTemporaryOrder     *walletv1.GetTemporaryRechargeOrderRequest
	temporaryOrderResp     *walletv1.H5RechargeOrderResponse
	lastSetMethodStatus    *walletv1.SetThirdPartyPaymentMethodStatusRequest
	lastUpdateRate         *walletv1.UpdateThirdPartyPaymentRateRequest
	lastSyncMethods        *walletv1.SyncThirdPartyPaymentMethodsRequest
	syncMethodsResp        *walletv1.SyncThirdPartyPaymentMethodsResponse
	updateRateRequests     []*walletv1.UpdateThirdPartyPaymentRateRequest
	lastCreateProduct      *walletv1.CreateRechargeProductRequest
}

func (m *mockPaymentWallet) ListRechargeBills(_ context.Context, req *walletv1.ListRechargeBillsRequest) (*walletv1.ListRechargeBillsResponse, error) {
	m.lastRechargeBills = req
	if m.rechargeBillsResp != nil {
		return m.rechargeBillsResp, nil
	}
	return &walletv1.ListRechargeBillsResponse{}, nil
}

func (m *mockPaymentWallet) GetRechargeBillSummary(_ context.Context, req *walletv1.GetRechargeBillSummaryRequest) (*walletv1.GetRechargeBillSummaryResponse, error) {
	if m.rechargeSummaryResp != nil {
		return m.rechargeSummaryResp, nil
	}
	return &walletv1.GetRechargeBillSummaryResponse{}, nil
}

func (m *mockPaymentWallet) GetRechargeBillOverview(_ context.Context, req *walletv1.GetRechargeBillOverviewRequest) (*walletv1.GetRechargeBillOverviewResponse, error) {
	m.rechargeOverviewCalls = append(m.rechargeOverviewCalls, req)
	if m.rechargeOverviewFn != nil {
		return m.rechargeOverviewFn(req)
	}
	return &walletv1.GetRechargeBillOverviewResponse{}, nil
}

func (m *mockPaymentWallet) ListThirdPartyPaymentChannels(_ context.Context, req *walletv1.ListThirdPartyPaymentChannelsRequest) (*walletv1.ListThirdPartyPaymentChannelsResponse, error) {
	m.lastThirdPartyChannels = req
	if m.thirdPartyChannelsResp != nil {
		return m.thirdPartyChannelsResp, nil
	}
	return &walletv1.ListThirdPartyPaymentChannelsResponse{}, nil
}

func (m *mockPaymentWallet) ListTemporaryRechargeOrders(_ context.Context, req *walletv1.ListTemporaryRechargeOrdersRequest) (*walletv1.ListTemporaryRechargeOrdersResponse, error) {
	m.lastTemporaryOrders = req
	if m.temporaryOrdersResp != nil {
		return m.temporaryOrdersResp, nil
	}
	return &walletv1.ListTemporaryRechargeOrdersResponse{}, nil
}

func (m *mockPaymentWallet) CreateTemporaryRechargeOrder(_ context.Context, req *walletv1.CreateTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) {
	m.lastCreateTemporary = req
	if m.createTemporaryResp != nil {
		return m.createTemporaryResp, nil
	}
	return &walletv1.H5RechargeOrderResponse{Order: &walletv1.ExternalRechargeOrder{
		OrderId:         "tmp_created",
		AppCode:         req.GetAppCode(),
		AudienceType:    "temporary",
		UsdMinorAmount:  req.GetUsdMinorAmount(),
		ProviderCode:    req.GetProviderCode(),
		PaymentMethodId: req.GetPaymentMethodId(),
		PayUrl:          "https://pay.example/tmp_created",
		Status:          "redirected",
	}}, nil
}

func (m *mockPaymentWallet) GetTemporaryRechargeOrder(_ context.Context, req *walletv1.GetTemporaryRechargeOrderRequest) (*walletv1.H5RechargeOrderResponse, error) {
	m.lastTemporaryOrder = req
	if m.temporaryOrderResp != nil {
		return m.temporaryOrderResp, nil
	}
	return &walletv1.H5RechargeOrderResponse{Order: &walletv1.ExternalRechargeOrder{OrderId: req.GetOrderId(), Status: "pending"}}, nil
}

func (m *mockPaymentWallet) SetThirdPartyPaymentMethodStatus(_ context.Context, req *walletv1.SetThirdPartyPaymentMethodStatusRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error) {
	m.lastSetMethodStatus = req
	status := "disabled"
	if req.GetEnabled() {
		status = "active"
	}
	return &walletv1.ThirdPartyPaymentMethodResponse{Method: &walletv1.ThirdPartyPaymentMethod{
		MethodId:     req.GetMethodId(),
		AppCode:      req.GetAppCode(),
		ProviderCode: "mifapay",
		ProviderName: "MiFaPay",
		CountryCode:  "SA",
		PayWay:       "Card",
		PayType:      "MADA",
		Status:       status,
	}}, nil
}

func (m *mockPaymentWallet) UpdateThirdPartyPaymentRate(_ context.Context, req *walletv1.UpdateThirdPartyPaymentRateRequest) (*walletv1.ThirdPartyPaymentMethodResponse, error) {
	m.lastUpdateRate = req
	m.updateRateRequests = append(m.updateRateRequests, req)
	return &walletv1.ThirdPartyPaymentMethodResponse{Method: &walletv1.ThirdPartyPaymentMethod{
		MethodId:          req.GetMethodId(),
		AppCode:           req.GetAppCode(),
		ProviderCode:      "mifapay",
		ProviderName:      "MiFaPay",
		CountryCode:       "SA",
		PayWay:            "Card",
		PayType:           "MADA",
		Status:            "active",
		UsdToCurrencyRate: req.GetUsdToCurrencyRate(),
	}}, nil
}

func (m *mockPaymentWallet) SyncThirdPartyPaymentMethods(_ context.Context, req *walletv1.SyncThirdPartyPaymentMethodsRequest) (*walletv1.SyncThirdPartyPaymentMethodsResponse, error) {
	m.lastSyncMethods = req
	if m.syncMethodsResp != nil {
		return m.syncMethodsResp, nil
	}
	return &walletv1.SyncThirdPartyPaymentMethodsResponse{ProviderCode: req.GetProviderCode()}, nil
}

type fakeExchangeRateClient struct {
	result exchangeRateResult
	err    error
}

func (f fakeExchangeRateClient) LatestUSDRates(context.Context, []string) (exchangeRateResult, error) {
	return f.result, f.err
}

func (m *mockPaymentWallet) CreateRechargeProduct(_ context.Context, req *walletv1.CreateRechargeProductRequest) (*walletv1.RechargeProductResponse, error) {
	m.lastCreateProduct = req
	return &walletv1.RechargeProductResponse{Product: &walletv1.RechargeProduct{
		AppCode:      req.GetAppCode(),
		ProductId:    9001,
		ProductCode:  "h5_seller_10",
		ProductName:  req.GetProductName(),
		Description:  req.GetDescription(),
		AudienceType: req.GetAudienceType(),
		Platform:     req.GetPlatform(),
		AmountMicro:  req.GetAmountMicro(),
		CoinAmount:   req.GetCoinAmount(),
		RegionIds:    append([]int64(nil), req.GetRegionIds()...),
		Enabled:      req.GetEnabled(),
		AppVisible:   req.GetAppVisible(),
		Status:       "active",
	}}, nil
}
