// Package mysqltest provides real wallet-service MySQL repositories for tests.
package mysqltest

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"strings"
	"testing"
	"time"

	"hyapp/internal/testutil/mysqlschema"
	"hyapp/pkg/appcode"
	"hyapp/services/wallet-service/internal/domain/ledger"
	mysqlstorage "hyapp/services/wallet-service/internal/storage/mysql"
)

// SetPointWithdrawalCoinSellerConfig seeds one app-scoped whitelist row for real ledger tests.
func (r *Repository) SetPointWithdrawalCoinSellerConfig(appCode string, sellerUserID int64, pointAmount int64, sellerCoinAmount int64, regionIDs []int64, countryCodes []string, status string, sortOrder int) {
	r.t.Helper()
	regionsJSON, err := json.Marshal(regionIDs)
	if err != nil {
		r.t.Fatalf("marshal point withdrawal regions failed: %v", err)
	}
	countriesJSON, err := json.Marshal(countryCodes)
	if err != nil {
		r.t.Fatalf("marshal point withdrawal countries failed: %v", err)
	}
	if status == "" {
		status = "active"
	}
	nowMS := time.Now().UnixMilli()
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO point_withdrawal_coin_seller_configs (
		  app_code, seller_user_id, sort_order, point_amount, seller_coin_amount, service_region_ids, service_country_codes,
		  status, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
		  sort_order = VALUES(sort_order), point_amount = VALUES(point_amount), seller_coin_amount = VALUES(seller_coin_amount),
		  service_region_ids = VALUES(service_region_ids), service_country_codes = VALUES(service_country_codes),
		  status = VALUES(status), updated_at_ms = VALUES(updated_at_ms)`,
		appCode, sellerUserID, sortOrder, pointAmount, sellerCoinAmount, regionsJSON, countriesJSON, status, nowMS, nowMS,
	); err != nil {
		r.t.Fatalf("seed point withdrawal coin seller config failed: %v", err)
	}
}

// Repository wraps the production MySQL wallet repository with test seed helpers.
type Repository struct {
	*mysqlstorage.Repository

	t      testing.TB
	schema *mysqlschema.Schema
}

type VipRewardResourceSeed struct {
	ResourceID    int64
	ResourceCode  string
	ResourceType  string
	Name          string
	GrantStrategy string
	SortOrder     int
}

// CountRows returns a simple count from a known wallet table for persistence assertions.
func (r *Repository) CountRows(table string, where string, args ...any) int {
	r.t.Helper()

	query := "SELECT COUNT(*) FROM " + validateTableName(table)
	if where != "" {
		query += " WHERE " + where
	}
	var count int
	if err := r.schema.DB.QueryRowContext(context.Background(), query, args...).Scan(&count); err != nil {
		r.t.Fatalf("count %s rows failed: %v\nquery=%s", table, err, query)
	}
	return count
}

// SetTransactionRequestHashForApp 模拟 request_hash 口径升级前已成功落库的历史交易。
// 使用 (app_code, command_id) 唯一索引定点更新，测试不会制造全表扫描。
func (r *Repository) SetTransactionRequestHashForApp(app string, commandID string, requestHash string) {
	r.t.Helper()
	result, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE wallet_transactions
		SET request_hash = ?
		WHERE app_code = ? AND command_id = ?
	`, requestHash, appcode.Normalize(app), commandID)
	if err != nil {
		r.t.Fatalf("set transaction request hash failed: %v", err)
	}
	affected, err := result.RowsAffected()
	if err != nil || affected != 1 {
		r.t.Fatalf("set transaction request hash affected %d rows: %v", affected, err)
	}
}

// TransactionCreatedAtMS 读取 wallet owner 固化的交易创建时间，供跨服务契约测试核对回执 paid_at_ms。
// 测试只按唯一 command_id 查询，避免把服务时钟近似值误当成账务事实。
func (r *Repository) TransactionCreatedAtMS(commandID string) int64 {
	r.t.Helper()
	var createdAtMS int64
	if err := r.schema.DB.QueryRowContext(context.Background(), `
		SELECT created_at_ms
		FROM wallet_transactions
		WHERE command_id = ?
	`, commandID).Scan(&createdAtMS); err != nil {
		r.t.Fatalf("query wallet transaction created_at_ms failed: %v", err)
	}
	return createdAtMS
}

// RemoveGiftPaidAtMetadata 模拟 paid_at_ms 契约上线前创建的历史 gift transaction。
// 交易主表 created_at_ms 保留不变，重放必须从 owner 主表恢复同一个时间。
func (r *Repository) RemoveGiftPaidAtMetadata(commandID string) {
	r.t.Helper()
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE wallet_transactions
		SET metadata_json = JSON_REMOVE(metadata_json, '$.paid_at_ms')
		WHERE command_id = ?
	`, commandID); err != nil {
		r.t.Fatalf("remove gift paid_at_ms metadata failed: %v", err)
	}
}

// SetRechargeProductCode overwrites product_code to model legacy rows created before code matched product_name.
func (r *Repository) SetRechargeProductCode(productID int64, productCode string) {
	r.t.Helper()
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE wallet_recharge_products
		SET product_code = ?
		WHERE product_id = ?`,
		productCode, productID,
	); err != nil {
		r.t.Fatalf("set recharge product code failed: %v", err)
	}
}

func (r *Repository) ActiveResourceEntitlement(userID int64, resourceID int64) (expiresAtMS int64, quantity int64, remainingQuantity int64) {
	r.t.Helper()

	err := r.schema.DB.QueryRowContext(context.Background(), `
		SELECT expires_at_ms, quantity, remaining_quantity
		FROM user_resource_entitlements
		WHERE user_id = ? AND resource_id = ? AND status = 'active'
		ORDER BY expires_at_ms DESC, created_at_ms DESC
		LIMIT 1
	`, userID, resourceID).Scan(&expiresAtMS, &quantity, &remainingQuantity)
	if err != nil {
		r.t.Fatalf("query active resource entitlement failed: %v", err)
	}
	return expiresAtMS, quantity, remainingQuantity
}

// ExpireEntitlement moves a granted entitlement into the past for expiry-path assertions.
func (r *Repository) ExpireEntitlement(entitlementID string, expiresAtMS int64) {
	r.t.Helper()

	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE user_resource_entitlements
		SET expires_at_ms = ?, updated_at_ms = ?
		WHERE entitlement_id = ?`,
		expiresAtMS, time.Now().UnixMilli(), entitlementID,
	); err != nil {
		r.t.Fatalf("expire entitlement failed: %v", err)
	}
}

// TamperPinnedGiftSnapshot mutates wallet-owned bytes without updating their hash so integrity fail-closed
// behaviour can be asserted through the public send-gift service.
func (r *Repository) TamperPinnedGiftSnapshot(entitlementID string) {
	r.t.Helper()
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE user_resource_entitlements
		SET gift_snapshot_json = JSON_SET(gift_snapshot_json, '$[0].CoinPrice', 999999), updated_at_ms = ?
		WHERE entitlement_id = ?`, time.Now().UnixMilli(), entitlementID); err != nil {
		r.t.Fatalf("tamper pinned gift snapshot failed: %v", err)
	}
}

// SetResourceGrantStatus lets service tests model legacy or abnormal grant rows without bypassing production grant creation.
func (r *Repository) SetResourceGrantStatus(grantID string, status string) {
	r.t.Helper()

	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE resource_grants
		SET status = ?, updated_at_ms = ?
		WHERE grant_id = ?`,
		status, time.Now().UnixMilli(), grantID,
	); err != nil {
		r.t.Fatalf("set resource grant status failed: %v", err)
	}
}

// HostSalarySettlementIDs returns the persisted settlement IDs for one host/cycle in creation order.
func (r *Repository) HostSalarySettlementIDs(userID int64, cycleKey string) []string {
	r.t.Helper()

	rows, err := r.schema.DB.QueryContext(context.Background(), `
		SELECT settlement_id
		FROM host_salary_settlement_records
		WHERE user_id = ? AND cycle_key = ?
		ORDER BY created_at_ms ASC, settlement_id ASC
	`, userID, cycleKey)
	if err != nil {
		r.t.Fatalf("query host salary settlement ids failed: %v", err)
	}
	defer rows.Close()

	var ids []string
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			r.t.Fatalf("scan host salary settlement id failed: %v", err)
		}
		ids = append(ids, id)
	}
	if err := rows.Err(); err != nil {
		r.t.Fatalf("iterate host salary settlement ids failed: %v", err)
	}
	return ids
}

// MoveHostPeriodCycle merges a real gift-created current-cycle account into another cycle for month-boundary tests.
func (r *Repository) MoveHostPeriodCycle(userID int64, fromCycle string, toCycle string) {
	r.t.Helper()
	if fromCycle == "" || toCycle == "" || fromCycle == toCycle {
		return
	}

	tx, err := r.schema.DB.BeginTx(context.Background(), nil)
	if err != nil {
		r.t.Fatalf("begin move host period cycle failed: %v", err)
	}
	defer func() { _ = tx.Rollback() }()

	var sourceTotal int64
	var sourceGiftTotal int64
	var sourceLastGiftAt int64
	var sourceUpdatedAt int64
	err = tx.QueryRowContext(context.Background(), `
		SELECT total_diamonds, gift_diamond_total, last_gift_at_ms, updated_at_ms
		FROM host_period_diamond_accounts
		WHERE user_id = ? AND cycle_key = ?
		FOR UPDATE
	`, userID, fromCycle).Scan(&sourceTotal, &sourceGiftTotal, &sourceLastGiftAt, &sourceUpdatedAt)
	if err != nil {
		r.t.Fatalf("read source host period cycle failed: %v", err)
	}

	var targetTotal int64
	err = tx.QueryRowContext(context.Background(), `
		SELECT total_diamonds
		FROM host_period_diamond_accounts
		WHERE user_id = ? AND cycle_key = ?
		FOR UPDATE
	`, userID, toCycle).Scan(&targetTotal)
	if err != nil {
		if err == sql.ErrNoRows {
			if _, err := tx.ExecContext(context.Background(), `
				UPDATE host_period_diamond_accounts
				SET cycle_key = ?
				WHERE user_id = ? AND cycle_key = ?
			`, toCycle, userID, fromCycle); err != nil {
				r.t.Fatalf("move first host period account failed: %v", err)
			}
		} else {
			r.t.Fatalf("read target host period cycle failed: %v", err)
		}
	} else {
		if _, err := tx.ExecContext(context.Background(), `
			UPDATE host_period_diamond_accounts
			SET total_diamonds = total_diamonds + ?,
			    gift_diamond_total = gift_diamond_total + ?,
			    version = version + 1,
			    last_gift_at_ms = ?,
			    updated_at_ms = ?
			WHERE user_id = ? AND cycle_key = ?
		`, sourceTotal, sourceGiftTotal, sourceLastGiftAt, sourceUpdatedAt, userID, toCycle); err != nil {
			r.t.Fatalf("merge target host period account failed: %v", err)
		}
		if _, err := tx.ExecContext(context.Background(), `
			DELETE FROM host_period_diamond_accounts
			WHERE user_id = ? AND cycle_key = ?
		`, userID, fromCycle); err != nil {
			r.t.Fatalf("delete merged source host period account failed: %v", err)
		}
	}
	if _, err := tx.ExecContext(context.Background(), `
		UPDATE host_period_diamond_entries
		SET cycle_key = ?
		WHERE user_id = ? AND cycle_key = ?
	`, toCycle, userID, fromCycle); err != nil {
		r.t.Fatalf("move host period entries failed: %v", err)
	}
	if err := tx.Commit(); err != nil {
		r.t.Fatalf("commit move host period cycle failed: %v", err)
	}
}

// HostPeriodGiftDiamondTotal returns the current accumulated gift diamonds for a host.
func (r *Repository) HostPeriodGiftDiamondTotal(userID int64) int64 {
	r.t.Helper()
	var total int64
	err := r.schema.DB.QueryRowContext(context.Background(), `
		SELECT COALESCE(SUM(gift_diamond_total), 0)
		FROM host_period_diamond_accounts
		WHERE user_id = ?`, userID).Scan(&total)
	if err != nil {
		r.t.Fatalf("query host period gift diamond total failed: %v", err)
	}
	return total
}

// InsertRawGiftPrice allows tests to seed inactive or future prices without service helpers.
func (r *Repository) InsertRawGiftPrice(giftID string, priceVersion string, status string, effectiveAtMs int64) {
	r.t.Helper()

	nowMs := time.Now().UnixMilli()
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO wallet_gift_prices (
			gift_id, price_version, status, coin_price, gift_point_amount, heat_value,
			effective_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, 1, 1, 1, ?, ?, ?)
	`, giftID, priceVersion, status, effectiveAtMs, nowMs, nowMs)
	if err != nil {
		r.t.Fatalf("insert raw gift price failed: %v", err)
	}
}

// NewRepository creates an isolated wallet schema and opens the production repository.
func NewRepository(t testing.TB) *Repository {
	t.Helper()

	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "WALLET_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     initDBPath(t),
		DatabasePrefix: "hyapp_wallet_test",
	})
	repository, err := mysqlstorage.Open(context.Background(), schema.DSN)
	if err != nil {
		t.Fatalf("open wallet mysql repository failed: %v", err)
	}

	wrapped := &Repository{Repository: repository, t: t, schema: schema}
	t.Cleanup(wrapped.Close)
	return wrapped
}

// Close shuts down the repository; mysqlschema owns schema cleanup.
func (r *Repository) Close() {
	r.t.Helper()

	if r.Repository != nil {
		_ = r.Repository.Close()
	}
}

// SetBalance prepares a sender COIN account for debit tests.
func (r *Repository) SetBalance(userID int64, balance int64) {
	r.SetAssetBalanceForApp("lalu", userID, "COIN", balance)
}

// SetBalanceForApp prepares a sender COIN account under the requested app scope.
func (r *Repository) SetBalanceForApp(appCode string, userID int64, balance int64) {
	r.SetAssetBalanceForApp(appCode, userID, "COIN", balance)
}

// ExpireRedPacket moves one packet past its deadline without bypassing the production refund path.
func (r *Repository) ExpireRedPacket(appCode string, packetID string) {
	r.t.Helper()
	nowMS := time.Now().UTC().Add(-time.Second).UnixMilli()
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE red_packets
		SET expires_at_ms = ?, updated_at_ms = ?
		WHERE app_code = ? AND packet_id = ?`,
		nowMS, nowMS, strings.ToLower(strings.TrimSpace(appCode)), packetID,
	); err != nil {
		r.t.Fatalf("expire red packet failed: %v", err)
	}
}

func (r *Repository) SeedVIPRewardGroup(level int32, groupID int64, resources []VipRewardResourceSeed) {
	r.t.Helper()

	nowMs := time.Now().UnixMilli()
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO resource_groups (
			group_id, group_code, name, status, description, sort_order,
			created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, 'active', '', 0, 0, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
			status = VALUES(status),
			updated_at_ms = VALUES(updated_at_ms)
	`, groupID, fmt.Sprintf("vip_test_group_%d", groupID), fmt.Sprintf("VIP test group %d", groupID), nowMs, nowMs)
	if err != nil {
		r.t.Fatalf("seed vip reward group failed: %v", err)
	}
	if _, err := r.schema.DB.ExecContext(context.Background(), `DELETE FROM resource_group_items WHERE group_id = ?`, groupID); err != nil {
		r.t.Fatalf("clear vip reward group items failed: %v", err)
	}
	for index, resource := range resources {
		sortOrder := resource.SortOrder
		if sortOrder == 0 {
			sortOrder = (index + 1) * 10
		}
		name := resource.Name
		if name == "" {
			name = resource.ResourceCode
		}
		_, err := r.schema.DB.ExecContext(context.Background(), `
			INSERT INTO resources (
				resource_id, resource_code, resource_type, name, status,
				grantable, manager_grant_enabled, grant_strategy, wallet_asset_type,
				wallet_asset_amount, price_type, coin_price, gift_point_amount,
				usage_scope_json, asset_url, preview_url, animation_url,
				metadata_json, sort_order, created_by_user_id, updated_by_user_id,
				created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, 'active', TRUE, TRUE, ?, '', 0, '', 0, 0,
				JSON_ARRAY(), '', '', '', NULL, ?, 0, 0, ?, ?)
			ON DUPLICATE KEY UPDATE
				resource_type = VALUES(resource_type),
				name = VALUES(name),
				status = VALUES(status),
				grant_strategy = VALUES(grant_strategy),
				sort_order = VALUES(sort_order),
				updated_at_ms = VALUES(updated_at_ms)
		`, resource.ResourceID, resource.ResourceCode, resource.ResourceType, name, resource.GrantStrategy, sortOrder, nowMs, nowMs)
		if err != nil {
			r.t.Fatalf("seed vip reward resource failed: %v", err)
		}
		_, err = r.schema.DB.ExecContext(context.Background(), `
			INSERT INTO resource_group_items (
				group_id, item_type, resource_id, wallet_asset_type, wallet_asset_amount,
				quantity, duration_ms, sort_order, created_at_ms, updated_at_ms
			) VALUES (?, 'resource', ?, '', 0, 1, 0, ?, ?, ?)
		`, groupID, resource.ResourceID, sortOrder, nowMs, nowMs)
		if err != nil {
			r.t.Fatalf("seed vip reward group item failed: %v", err)
		}
	}
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE vip_levels
		SET reward_resource_group_id = ?, updated_at_ms = ?
		WHERE level = ?
	`, groupID, nowMs, level); err != nil {
		r.t.Fatalf("update vip level reward group failed: %v", err)
	}
}

// SetAssetBalance prepares a sender account for debit tests.
func (r *Repository) SetAssetBalance(userID int64, assetType string, balance int64) {
	r.SetAssetBalanceForApp("lalu", userID, assetType, balance)
}

// SetAssetBalanceForApp prepares an account in the same app scope that production ledger code will query.
func (r *Repository) SetAssetBalanceForApp(appCode string, userID int64, assetType string, balance int64) {
	r.t.Helper()

	nowMs := time.Now().UnixMilli()
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO wallet_accounts (app_code, user_id, asset_type, available_amount, frozen_amount, version, created_at_ms, updated_at_ms)
		VALUES (?, ?, ?, ?, 0, 1, ?, ?)
		ON DUPLICATE KEY UPDATE
			available_amount = VALUES(available_amount),
			frozen_amount = VALUES(frozen_amount),
			version = version + 1,
			updated_at_ms = VALUES(updated_at_ms)
	`, strings.TrimSpace(appCode), userID, assetType, balance, nowMs, nowMs)
	if err != nil {
		r.t.Fatalf("seed wallet balance failed: %v", err)
	}
}

// SeedPointWalletPolicy 写入 POINT 提现和兑换仍消费的 wallet 运行政策；礼物测试不得使用该夹具。
func (r *Repository) SeedPointWalletPolicy(appCode string, instanceCode string, regionID int64, status string, pointsPerUSD int64, minimumPoints int64, feeBPS int64) {
	r.t.Helper()

	nowMS := time.Now().UTC().UnixMilli()
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO wallet_policy_instances (
			app_code, instance_code, template_code, template_version, region_id, status,
			effective_from_ms, effective_to_ms, host_point_asset_type, host_point_ratio_ppm,
			agency_point_ratio_ppm, points_per_usd, withdraw_fee_bps, rule_json,
			published_by_admin_id, published_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, 'v1', ?, ?, 0, 0, 'POINT', 0, 0, ?, ?,
		          JSON_OBJECT('host', JSON_OBJECT('minimum_withdraw_points', ?)), 90001, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			status = VALUES(status),
			points_per_usd = VALUES(points_per_usd),
			withdraw_fee_bps = VALUES(withdraw_fee_bps),
			rule_json = VALUES(rule_json),
			published_at_ms = VALUES(published_at_ms),
			updated_at_ms = VALUES(updated_at_ms)
	`, strings.TrimSpace(appCode), strings.TrimSpace(instanceCode), strings.TrimSpace(instanceCode)+"_template",
		regionID, strings.TrimSpace(status), pointsPerUSD, feeBPS, minimumPoints, nowMS, nowMS, nowMS)
	if err != nil {
		r.t.Fatalf("seed POINT wallet policy failed: %v", err)
	}
}

// SetPointWalletPolicyRawSnapshot 只用于迁移回归：它在真实 wallet 运行表上构造已发布旧快照，
// 以便证明迁移只删指定模板、且不会损坏其他 App 的 POINT 财务参数。
func (r *Repository) SetPointWalletPolicyRawSnapshot(appCode string, instanceCode string, regionID int64, templateCode string, hostRatioPPM int64, agencyRatioPPM int64, ruleJSON string) {
	r.t.Helper()
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE wallet_policy_instances
		SET template_code = ?, host_point_ratio_ppm = ?, agency_point_ratio_ppm = ?, rule_json = CAST(? AS JSON)
		WHERE app_code = ? AND instance_code = ? AND region_id = ?`,
		strings.TrimSpace(templateCode), hostRatioPPM, agencyRatioPPM, ruleJSON,
		strings.TrimSpace(appCode), strings.TrimSpace(instanceCode), regionID,
	); err != nil {
		r.t.Fatalf("set raw POINT wallet policy snapshot failed: %v", err)
	}
}

// ApplySQLScript 在 mysqltest 的隔离真实 MySQL schema 中执行简单迁移脚本。
// 该帮助方法不忽略 SQL 错误，因此重复执行测试能真正覆盖幂等性。
func (r *Repository) ApplySQLScript(script string) {
	r.t.Helper()
	// 先移除独立行注释，否则注释中的分号会被误当成 SQL 边界。
	lines := strings.Split(script, "\n")
	statementsOnly := make([]string, 0, len(lines))
	for _, line := range lines {
		if strings.HasPrefix(strings.TrimSpace(line), "--") {
			continue
		}
		statementsOnly = append(statementsOnly, line)
	}
	for statement := range strings.SplitSeq(strings.Join(statementsOnly, "\n"), ";") {
		statement = strings.TrimSpace(statement)
		if statement == "" {
			continue
		}
		if _, err := r.schema.DB.ExecContext(context.Background(), statement); err != nil {
			r.t.Fatalf("apply SQL script failed: %v\nstatement:\n%s", err, statement)
		}
	}
}

// SetCoinSellerSalaryExchangeRateTier seeds an active salary transfer tier for coin seller transfer tests.
func (r *Repository) SetCoinSellerSalaryExchangeRateTier(appCode string, regionID int64, minUSDMinor int64, maxUSDMinor int64, coinPerUSD int64) {
	r.t.Helper()

	nowMs := time.Now().UnixMilli()
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO coin_seller_salary_exchange_rate_tiers (
			app_code, region_id, min_usd_minor, max_usd_minor, coin_per_usd,
			status, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, 'active', 10, 0, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
			coin_per_usd = VALUES(coin_per_usd),
			status = VALUES(status),
			updated_at_ms = VALUES(updated_at_ms)
	`, appCode, regionID, minUSDMinor, maxUSDMinor, coinPerUSD, nowMs, nowMs)
	if err != nil {
		r.t.Fatalf("seed coin seller salary exchange rate tier failed: %v", err)
	}
}

// CreateH5RechargeProduct 通过生产仓储写入 H5 档位，测试只提供角色、区域、美元微单位和金币数。
func (r *Repository) CreateH5RechargeProduct(audienceType string, regionID int64, amountMicro int64, coinAmount int64) ledger.RechargeProduct {
	r.t.Helper()

	product, err := r.CreateRechargeProduct(context.Background(), ledger.RechargeProductCommand{
		AppCode:        "lalu",
		AudienceType:   audienceType,
		AmountMicro:    amountMicro,
		CoinAmount:     coinAmount,
		ProductName:    fmt.Sprintf("H5 %s %d", audienceType, coinAmount),
		Platform:       ledger.RechargeProductPlatformWeb,
		RegionIDs:      []int64{regionID},
		Enabled:        true,
		OperatorUserID: 90001,
	})
	if err != nil {
		r.t.Fatalf("create h5 recharge product failed: %v", err)
	}
	return product
}

// ThirdPartyPaymentMethodID 返回 initdb/迁移种子里的真实 MiFaPay method_id，用于 H5 下单和跨国家拒绝测试。
func (r *Repository) ThirdPartyPaymentMethodID(countryCode string, payWay string, payType string) int64 {
	return r.ThirdPartyProviderPaymentMethodID(ledger.PaymentProviderMifaPay, countryCode, payWay, payType)
}

// ThirdPartyProviderPaymentMethodID 返回指定三方 provider 的真实 method_id，覆盖 V5Pay 和 MiFaPay 共用的支付方式模型。
func (r *Repository) ThirdPartyProviderPaymentMethodID(providerCode string, countryCode string, payWay string, payType string) int64 {
	r.t.Helper()

	var methodID int64
	err := r.schema.DB.QueryRowContext(context.Background(), `
		SELECT method_id
		FROM third_party_payment_methods
		WHERE app_code = 'lalu' AND provider_code = ?
		  AND country_code = ? AND pay_way = ? AND pay_type = ?
	LIMIT 1`,
		providerCode, countryCode, payWay, payType,
	).Scan(&methodID)
	if err != nil {
		r.t.Fatalf("query third party payment method %s/%s/%s/%s failed: %v", providerCode, countryCode, payWay, payType, err)
	}
	return methodID
}

// SetThirdPartyPaymentRate updates the production payment method row so service tests can lock provider amount semantics.
func (r *Repository) SetThirdPartyPaymentRate(methodID int64, rate string) {
	r.t.Helper()

	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE third_party_payment_methods
		SET usd_to_currency_rate = ?, updated_at_ms = ?
		WHERE method_id = ?`,
		strings.TrimSpace(rate), time.Now().UnixMilli(), methodID,
	); err != nil {
		r.t.Fatalf("set third party payment rate failed: %v", err)
	}
}

// SetGiftPrice overrides or inserts a gift settlement price for server-side billing tests.
func (r *Repository) SetGiftPrice(giftID string, priceVersion string, coinPrice int64, giftPointAmount int64, heatValue int64) {
	r.SetGiftPriceForApp("lalu", giftID, priceVersion, coinPrice, giftPointAmount, heatValue)
}

// SetGiftPriceForApp overrides or inserts a gift settlement price under the requested app scope.
func (r *Repository) SetGiftPriceForApp(appCode string, giftID string, priceVersion string, coinPrice int64, giftPointAmount int64, heatValue int64) {
	r.t.Helper()

	appCode = strings.TrimSpace(appCode)
	nowMs := time.Now().UnixMilli()
	// DebitGift 真实链路先校验 active gift_config，再读取 wallet_gift_prices；测试 seed 必须同时补齐资源、礼物配置和区域关系。
	result, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO resources (
			app_code, resource_code, resource_type, name, status, grantable, manager_grant_enabled,
			grant_strategy, price_type, coin_price, gift_point_amount, usage_scope_json,
			created_at_ms, updated_at_ms
		) VALUES (?, ?, 'gift', ?, 'active', TRUE, TRUE, 'new_entitlement', 'coin', ?, ?, '[]', ?, ?)
		ON DUPLICATE KEY UPDATE
			resource_id = LAST_INSERT_ID(resource_id),
			status = VALUES(status),
			coin_price = VALUES(coin_price),
			gift_point_amount = VALUES(gift_point_amount),
			updated_at_ms = VALUES(updated_at_ms)
	`, appCode, "test-gift-"+giftID, giftID, coinPrice, giftPointAmount, nowMs, nowMs)
	if err != nil {
		r.t.Fatalf("seed gift resource failed: %v", err)
	}
	resourceID, err := result.LastInsertId()
	if err != nil {
		r.t.Fatalf("read gift resource id failed: %v", err)
	}
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO gift_configs (
			app_code, gift_id, resource_id, status, name, sort_order, presentation_json,
			gift_type_code, effective_from_ms, effective_to_ms, effect_types_json,
			created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, 'active', ?, 0, JSON_OBJECT(), 'normal', 0, 0, JSON_ARRAY(), 0, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
			resource_id = VALUES(resource_id),
			status = VALUES(status),
			name = VALUES(name),
			gift_type_code = VALUES(gift_type_code),
			effective_from_ms = VALUES(effective_from_ms),
			effective_to_ms = VALUES(effective_to_ms),
			updated_at_ms = VALUES(updated_at_ms)
	`, appCode, giftID, resourceID, giftID, nowMs, nowMs); err != nil {
		r.t.Fatalf("seed gift config failed: %v", err)
	}
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO gift_config_regions (app_code, gift_id, region_id, created_at_ms, updated_at_ms)
		VALUES (?, ?, 0, ?, ?)
		ON DUPLICATE KEY UPDATE updated_at_ms = VALUES(updated_at_ms)
	`, appCode, giftID, nowMs, nowMs); err != nil {
		r.t.Fatalf("seed gift config region failed: %v", err)
	}
	_, err = r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO wallet_gift_prices (
			app_code, gift_id, price_version, status, coin_price, gift_point_amount, heat_value,
			effective_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, 'active', ?, ?, ?, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
			status = VALUES(status),
			coin_price = VALUES(coin_price),
			gift_point_amount = VALUES(gift_point_amount),
			heat_value = VALUES(heat_value),
			updated_at_ms = VALUES(updated_at_ms)
	`, appCode, giftID, priceVersion, coinPrice, giftPointAmount, heatValue, nowMs, nowMs)
	if err != nil {
		r.t.Fatalf("seed wallet gift price failed: %v", err)
	}
	// 生产启动不再生成任何倍率。测试礼物 fixture 必须像后台发布一样显式写入三类全局配置，
	// 让绝大多数账本测试聚焦自身语义；缺政策用例会再主动删除这些行。
	for _, giftTypeCode := range []string{"normal", "lucky", "super_lucky"} {
		r.SetGiftDiamondRatioForApp(appCode, 0, giftTypeCode, defaultGiftDiamondRatioPercent(giftTypeCode))
	}
}

// SetGiftType updates a seeded gift config to the requested gift type.
func (r *Repository) SetGiftType(giftID string, giftTypeCode string) {
	r.t.Helper()
	_, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE gift_configs
		SET gift_type_code = ?, updated_at_ms = ?
		WHERE gift_id = ?`, giftTypeCode, time.Now().UnixMilli(), giftID)
	if err != nil {
		r.t.Fatalf("set gift type failed: %v", err)
	}
}

// SetGiftDiamondRatio configures the gift diamond ratio for a room region and gift type.
func (r *Repository) SetGiftDiamondRatio(regionID int64, giftTypeCode string, ratioPercent string) {
	r.SetGiftDiamondRatioForApp("lalu", regionID, giftTypeCode, ratioPercent)
}

// SetGiftDiamondRatioForApp writes an app-isolated ratio so multi-tenant policy tests can prove the effective-region snapshot.
func (r *Repository) SetGiftDiamondRatioForApp(appCode string, regionID int64, giftTypeCode string, ratioPercent string) {
	r.t.Helper()
	nowMs := time.Now().UnixMilli()
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO gift_diamond_ratio_configs (
		  app_code, region_id, gift_type_code, status, ratio_percent, coin_return_ratio_percent,
		  created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, 'active', ?, ?, 0, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
		  status = VALUES(status),
		  ratio_percent = VALUES(ratio_percent),
		  updated_at_ms = VALUES(updated_at_ms)`,
		strings.TrimSpace(appCode), regionID, giftTypeCode, ratioPercent, defaultGiftReturnCoinRatioPercent(giftTypeCode), nowMs, nowMs)
	if err != nil {
		r.t.Fatalf("set gift diamond ratio failed: %v", err)
	}
}

// SetGiftReturnCoinRatio configures the receiver COIN return ratio without changing host-period diamond rules.
func (r *Repository) SetGiftReturnCoinRatio(regionID int64, giftTypeCode string, ratioPercent string) {
	r.t.Helper()
	nowMs := time.Now().UnixMilli()
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO gift_diamond_ratio_configs (
		  region_id, gift_type_code, status, ratio_percent, coin_return_ratio_percent,
		  created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
		) VALUES (?, ?, 'active', ?, ?, 0, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
		  status = VALUES(status),
		  coin_return_ratio_percent = VALUES(coin_return_ratio_percent),
		  updated_at_ms = VALUES(updated_at_ms)`,
		regionID, giftTypeCode, defaultGiftDiamondRatioPercent(giftTypeCode), ratioPercent, nowMs, nowMs)
	if err != nil {
		r.t.Fatalf("set gift return coin ratio failed: %v", err)
	}
}

// DeleteGiftDiamondRatiosForApp removes only test-fixture policy rows for fail-closed test cases.
func (r *Repository) DeleteGiftDiamondRatiosForApp(appCode string) {
	r.t.Helper()
	if _, err := r.schema.DB.ExecContext(context.Background(), `DELETE FROM gift_diamond_ratio_configs WHERE app_code = ?`, strings.TrimSpace(appCode)); err != nil {
		r.t.Fatalf("delete gift diamond ratios failed: %v", err)
	}
}

func defaultGiftDiamondRatioPercent(giftTypeCode string) string {
	switch strings.TrimSpace(giftTypeCode) {
	case "lucky":
		return "10.00"
	case "super_lucky":
		return "1.00"
	default:
		return "100.00"
	}
}

func defaultGiftReturnCoinRatioPercent(giftTypeCode string) string {
	switch strings.TrimSpace(giftTypeCode) {
	case "lucky":
		return "10.00"
	case "super_lucky":
		return "1.00"
	default:
		return "30.00"
	}
}

// SetRechargePolicy 配置区域充值定价，用于外部充值类事实测试。
func (r *Repository) SetRechargePolicy(regionID int64, policyVersion string, coinAmount int64, usdMinorAmount int64) {
	r.t.Helper()

	nowMs := time.Now().UnixMilli()
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO wallet_recharge_policies (
			region_id, policy_version, status, currency_code, coin_amount, usd_minor_amount,
			effective_from_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, 'active', 'USD', ?, ?, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
			status = VALUES(status),
			currency_code = VALUES(currency_code),
			coin_amount = VALUES(coin_amount),
			usd_minor_amount = VALUES(usd_minor_amount),
			updated_at_ms = VALUES(updated_at_ms)
	`, regionID, policyVersion, coinAmount, usdMinorAmount, nowMs, nowMs)
	if err != nil {
		r.t.Fatalf("seed wallet recharge policy failed: %v", err)
	}
}

// SetRechargeStats prepares cumulative recharge stats for recharge-related wallet tests.
func (r *Repository) SetRechargeStats(userID int64, totalCoinAmount int64) {
	r.t.Helper()

	nowMs := time.Now().UnixMilli()
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO wallet_user_recharge_stats (
			user_id, recharge_count, first_transaction_id, first_recharged_at_ms,
			total_coin_amount, total_usd_minor_amount, created_at_ms, updated_at_ms
		) VALUES (?, 1, 'test_recharge', ?, ?, 0, ?, ?)
		ON DUPLICATE KEY UPDATE
			total_coin_amount = VALUES(total_coin_amount),
			updated_at_ms = VALUES(updated_at_ms)
	`, userID, nowMs, totalCoinAmount, nowMs, nowMs)
	if err != nil {
		r.t.Fatalf("seed wallet recharge stats failed: %v", err)
	}
}

// SetVIPLevelRechargeThreshold writes a legacy VIP threshold directly, so tests can verify old rows no longer affect purchase eligibility.
func (r *Repository) SetVIPLevelRechargeThreshold(level int32, totalCoinAmount int64) {
	r.t.Helper()

	nowMs := time.Now().UnixMilli()
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE vip_levels
		SET required_recharge_coin_amount = ?, updated_at_ms = ?
		WHERE level = ?`,
		totalCoinAmount, nowMs, level,
	); err != nil {
		r.t.Fatalf("set vip level recharge threshold failed: %v", err)
	}
}

// SetVIPLevelStatusForApp 只用于测试需要经过真实购买/体验卡入口的等级启停。
func (r *Repository) SetVIPLevelStatusForApp(appCode string, level int32, status string, priceCoin int64) {
	r.t.Helper()
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE vip_levels
		SET status = ?, price_coin = ?, updated_at_ms = ?
		WHERE app_code = ? AND level = ?`,
		status, priceCoin, time.Now().UTC().UnixMilli(), appCode, level,
	); err != nil {
		r.t.Fatalf("set vip level status failed: %v", err)
	}
}

// BindVIPBenefitResourceForApp 将既有等级权益绑定到测试资源，覆盖购买、自动佩戴和展示投影的真实链路。
func (r *Repository) BindVIPBenefitResourceForApp(appCode string, level int32, benefitCode string, resourceID int64, resourceType string, trialEnabled bool) {
	r.t.Helper()
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE vip_level_benefits
		SET resource_id = ?, resource_type = ?, trial_enabled = ?, auto_equip = TRUE,
		    status = 'active', updated_at_ms = ?
		WHERE app_code = ? AND level = ? AND benefit_code = ?`,
		resourceID, resourceType, trialEnabled, time.Now().UTC().UnixMilli(),
		appCode, level, benefitCode,
	); err != nil {
		r.t.Fatalf("bind vip benefit resource failed: %v", err)
	}
}

// SetVIPDailyCoinRebateAmount 写入测试专用等级金额并递增 program 版本，模拟后台完整配置发布。
// amount<=0 时仍写入，供服务端校验/运行防线测试构造异常历史数据。
func (r *Repository) SetVIPDailyCoinRebateAmount(appCode string, level int32, amount int64, status string) {
	r.t.Helper()
	nowMS := time.Now().UTC().UnixMilli()
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO vip_level_benefits (
		  app_code, level, benefit_code, name, benefit_type, unlock_level, status,
		  trial_enabled, resource_id, resource_type, execution_scope, auto_equip,
		  sort_order, metadata_json, created_at_ms, updated_at_ms
		) VALUES (?, ?, 'daily_coin_rebate', '金币返现', 'function', ?, ?, FALSE, 0, '', 'wallet', FALSE, 250,
		          JSON_OBJECT('coin_amount', ?), ?, ?)
		ON DUPLICATE KEY UPDATE
		  status = VALUES(status), trial_enabled = FALSE, execution_scope = 'wallet',
		  metadata_json = VALUES(metadata_json), updated_at_ms = VALUES(updated_at_ms)`,
		appCode, level, level, status, amount, nowMS, nowMS,
	)
	if err != nil {
		r.t.Fatalf("set vip daily coin rebate amount failed: %v", err)
	}
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE vip_program_configs
		SET config_version = config_version + 1, updated_at_ms = ?
		WHERE app_code = ?`, nowMS, appCode); err != nil {
		r.t.Fatalf("advance vip program version failed: %v", err)
	}
}

// SeedPaidVIPAtWindow 构造有完整 history 的付费 VIP，用于精确覆盖 UTC 日切边界。
func (r *Repository) SeedPaidVIPAtWindow(appCode string, userID int64, level int32, startedAtMS int64, expiresAtMS int64) {
	r.t.Helper()
	var name string
	var programType string
	var configVersion int64
	if err := r.schema.DB.QueryRowContext(context.Background(), `
		SELECT l.name, p.program_type, p.config_version
		FROM vip_levels l
		JOIN vip_program_configs p ON p.app_code = l.app_code
		WHERE l.app_code = ? AND l.level = ?`, appCode, level,
	).Scan(&name, &programType, &configVersion); err != nil {
		r.t.Fatalf("read vip seed level failed: %v", err)
	}
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO user_vip_memberships (
		  app_code, user_id, level, name, status, program_type, config_version,
		  started_at_ms, expires_at_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
		  level = VALUES(level), name = VALUES(name), status = 'active',
		  program_type = VALUES(program_type), config_version = VALUES(config_version),
		  started_at_ms = VALUES(started_at_ms), expires_at_ms = VALUES(expires_at_ms),
		  updated_at_ms = VALUES(updated_at_ms)`,
		appCode, userID, level, name, programType, configVersion,
		startedAtMS, expiresAtMS, startedAtMS, startedAtMS,
	)
	if err != nil {
		r.t.Fatalf("seed paid vip membership failed: %v", err)
	}
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO user_vip_history (
		  app_code, user_id, action, previous_level, new_level, previous_expires_at_ms,
		  new_expires_at_ms, program_type, config_version, order_id, created_at_ms
		) VALUES (?, ?, 'test_seed', 0, ?, 0, ?, ?, ?, '', ?)`,
		appCode, userID, level, expiresAtMS, programType, configVersion, startedAtMS,
	); err != nil {
		r.t.Fatalf("seed paid vip history failed: %v", err)
	}
}

// SeedLegacyPaidVIPAtWindow 模拟 060 新增有效期列后的存量历史：会员主表仍有完整窗口，
// 但旧 history 因 ALTER DEFAULT 只有 0。运行时不得把这个 0 当作“日切时已过期”。
func (r *Repository) SeedLegacyPaidVIPAtWindow(appCode string, userID int64, level int32, startedAtMS int64, expiresAtMS int64) {
	r.t.Helper()
	r.SeedPaidVIPAtWindow(appCode, userID, level, startedAtMS, expiresAtMS)
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		UPDATE user_vip_history
		SET action = 'legacy_test_seed', previous_expires_at_ms = 0, new_expires_at_ms = 0
		WHERE app_code = ? AND user_id = ? AND action = 'test_seed' AND created_at_ms = ?`,
		appCode, userID, startedAtMS,
	); err != nil {
		r.t.Fatalf("convert paid vip history to legacy zero-expiry snapshot failed: %v", err)
	}
}

// ApplyVIPHistoryMigrationBaseline 在隔离真实 MySQL schema 中复制 060 的基线 INSERT。
// baselineAtMS 可固定在待测日切前，用于验证后续日切能读取该完整快照；重复调用不应增行。
func (r *Repository) ApplyVIPHistoryMigrationBaseline(baselineAtMS int64) {
	r.t.Helper()
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO user_vip_history (
		  app_code, user_id, action, previous_level, new_level, previous_expires_at_ms,
		  new_expires_at_ms, program_type, config_version, order_id, created_at_ms
		)
		SELECT
		  memberships.app_code, memberships.user_id, 'migration_baseline', 0,
		  memberships.level, 0, memberships.expires_at_ms, memberships.program_type,
		  memberships.config_version, 'vip_configurable_060_baseline', ?
		FROM user_vip_memberships AS memberships
		WHERE memberships.level > 0
		  AND memberships.expires_at_ms > 0
		  AND NOT EXISTS (
		    SELECT 1
		    FROM user_vip_history AS complete_history
		    WHERE complete_history.app_code = memberships.app_code
		      AND complete_history.user_id = memberships.user_id
		      AND complete_history.new_level > 0
		      AND complete_history.new_expires_at_ms > 0
		  )
		  AND NOT EXISTS (
		    SELECT 1
		    FROM user_vip_history AS existing_baseline
		    WHERE existing_baseline.app_code = memberships.app_code
		      AND existing_baseline.user_id = memberships.user_id
		      AND existing_baseline.order_id = 'vip_configurable_060_baseline'
		  )`, baselineAtMS,
	); err != nil {
		r.t.Fatalf("apply vip history migration baseline failed: %v", err)
	}
}

// WalletOutboxPayload returns the latest payload for a precise event assertion.
func (r *Repository) WalletOutboxPayload(appCode string, eventType string, userID int64) string {
	r.t.Helper()
	var payload string
	if err := r.schema.DB.QueryRowContext(context.Background(), `
		SELECT CAST(payload AS CHAR)
		FROM wallet_outbox
		WHERE app_code = ? AND event_type = ? AND user_id = ?
		ORDER BY created_at_ms DESC, event_id DESC LIMIT 1`,
		appCode, eventType, userID,
	).Scan(&payload); err != nil {
		r.t.Fatalf("read wallet outbox payload failed: %v", err)
	}
	return payload
}

// SetHostSalaryPolicy seeds the wallet runtime policy snapshot used by salary settlement tests.
func (r *Repository) SetHostSalaryPolicy(policy ledger.HostSalaryPolicy) {
	r.SetHostSalaryPolicyForApp("lalu", policy)
}

// SetHostSalaryPolicyForApp 模拟 Admin 向 wallet 发布一份指定 App 的不可变月政策和周期绑定。
func (r *Repository) SetHostSalaryPolicyForApp(appCode string, policy ledger.HostSalaryPolicy) {
	r.t.Helper()
	appCode = strings.TrimSpace(appCode)
	if appCode == "" {
		appCode = "lalu"
	}

	nowMs := time.Now().UnixMilli()
	if policy.PolicyID == 0 {
		policy.PolicyID = 1
	}
	if strings.TrimSpace(policy.CycleKey) == "" {
		if policy.EffectiveFromMs > 0 {
			policy.CycleKey = time.UnixMilli(policy.EffectiveFromMs).UTC().Format("2006-01")
		} else {
			policy.CycleKey = time.Now().UTC().Format("2006-01")
		}
	}
	if policy.PolicyVersion == 0 {
		policy.PolicyVersion = policy.PolicyID
	}
	if policy.Status == "" {
		policy.Status = "active"
	}
	if policy.SettlementMode == "" {
		policy.SettlementMode = ledger.HostSalarySettlementModeDaily
	}
	if policy.SettlementTriggerMode == "" {
		// 测试默认覆盖生产 cron 路径；需要验证人工政策时再显式传 manual。
		policy.SettlementTriggerMode = ledger.HostSalarySettlementTriggerAutomatic
	}
	if policy.GiftCoinToDiamondRatio == "" {
		policy.GiftCoinToDiamondRatio = "1"
	}
	if strings.TrimSpace(policy.PolicyType) == "" {
		policy.PolicyType = ledger.HostPolicyTypeSalaryDiamond
	}
	if policy.ResidualDiamondToUSDRate == "" {
		policy.ResidualDiamondToUSDRate = "0"
	}
	if policy.CoinSellerWithdrawalLimitPeriod == "" {
		policy.CoinSellerWithdrawalLimitPeriod = "month"
	}
	if policy.PlatformWithdrawalLimitPeriod == "" {
		policy.PlatformWithdrawalLimitPeriod = "month"
	}
	legacyPointPolicy := policy.PolicyType == ledger.HostPolicyTypePointDiamond &&
		policy.CoinSellerMinimumWithdrawUSDMinor == 0 &&
		policy.PlatformMinimumWithdrawUSDMinor == 0
	if policy.CoinSellerMinimumWithdrawUSDMinor <= 0 {
		policy.CoinSellerMinimumWithdrawUSDMinor = policy.MinimumWithdrawUSDMinor
	}
	if policy.PlatformMinimumWithdrawUSDMinor <= 0 {
		policy.PlatformMinimumWithdrawUSDMinor = policy.MinimumWithdrawUSDMinor
	}
	policy.MinimumWithdrawUSDMinor = policy.PlatformMinimumWithdrawUSDMinor
	if legacyPointPolicy {
		// 既有测试夹具创建于开关引入前，语义等同线上迁移默认开启；新关闭场景需显式填写两个渠道门槛。
		policy.DiamondExchangeEnabled = true
	}
	_, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO host_agency_salary_policies (
			app_code, policy_id, cycle_key, policy_version, name, policy_type, region_id, status, settlement_mode, settlement_trigger_mode, gift_coin_to_diamond_ratio,
			point_diamonds_per_usd, coins_per_usd, minimum_withdraw_usd_minor,
			coin_seller_minimum_withdraw_usd_minor, platform_minimum_withdraw_usd_minor, diamond_exchange_enabled,
			withdraw_fee_bps, agency_point_share_bps,
			residual_diamond_to_usd_rate, coin_seller_withdrawal_limit_period, coin_seller_withdrawal_limit_count,
			platform_withdrawal_limit_period, platform_withdrawal_limit_count, platform_withdrawal_allowed_days,
			effective_from_ms, effective_to_ms, created_at_ms, updated_at_ms
		) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			cycle_key = VALUES(cycle_key),
			policy_version = VALUES(policy_version),
			name = VALUES(name),
			policy_type = VALUES(policy_type),
			region_id = VALUES(region_id),
			status = VALUES(status),
			settlement_mode = VALUES(settlement_mode),
			settlement_trigger_mode = VALUES(settlement_trigger_mode),
			gift_coin_to_diamond_ratio = VALUES(gift_coin_to_diamond_ratio),
			point_diamonds_per_usd = VALUES(point_diamonds_per_usd),
			coins_per_usd = VALUES(coins_per_usd),
			minimum_withdraw_usd_minor = VALUES(minimum_withdraw_usd_minor),
			coin_seller_minimum_withdraw_usd_minor = VALUES(coin_seller_minimum_withdraw_usd_minor),
			platform_minimum_withdraw_usd_minor = VALUES(platform_minimum_withdraw_usd_minor),
			diamond_exchange_enabled = VALUES(diamond_exchange_enabled),
			withdraw_fee_bps = VALUES(withdraw_fee_bps),
			agency_point_share_bps = VALUES(agency_point_share_bps),
			residual_diamond_to_usd_rate = VALUES(residual_diamond_to_usd_rate),
			coin_seller_withdrawal_limit_period = VALUES(coin_seller_withdrawal_limit_period),
			coin_seller_withdrawal_limit_count = VALUES(coin_seller_withdrawal_limit_count),
			platform_withdrawal_limit_period = VALUES(platform_withdrawal_limit_period),
			platform_withdrawal_limit_count = VALUES(platform_withdrawal_limit_count),
			platform_withdrawal_allowed_days = VALUES(platform_withdrawal_allowed_days),
			effective_from_ms = VALUES(effective_from_ms),
			effective_to_ms = VALUES(effective_to_ms),
			updated_at_ms = VALUES(updated_at_ms)
	`, appCode, policy.PolicyID, policy.CycleKey, policy.PolicyVersion, policy.Name, policy.PolicyType, policy.RegionID, policy.Status, policy.SettlementMode, policy.SettlementTriggerMode, policy.GiftCoinToDiamondRatio,
		policy.PointDiamondsPerUSD, policy.CoinsPerUSD, policy.MinimumWithdrawUSDMinor,
		policy.CoinSellerMinimumWithdrawUSDMinor, policy.PlatformMinimumWithdrawUSDMinor, policy.DiamondExchangeEnabled,
		policy.WithdrawFeeBPS, policy.AgencyPointShareBPS, policy.ResidualDiamondToUSDRate,
		policy.CoinSellerWithdrawalLimitPeriod, policy.CoinSellerWithdrawalLimitCount,
		policy.PlatformWithdrawalLimitPeriod, policy.PlatformWithdrawalLimitCount, policy.PlatformWithdrawalAllowedDays,
		policy.EffectiveFromMs, policy.EffectiveToMs, nowMs, nowMs)
	if err != nil {
		r.t.Fatalf("seed host salary policy failed: %v", err)
	}
	if _, err := r.schema.DB.ExecContext(context.Background(), `DELETE FROM host_agency_salary_policy_levels WHERE app_code = ? AND policy_id = ? AND policy_version = ?`, appCode, policy.PolicyID, policy.PolicyVersion); err != nil {
		r.t.Fatalf("clear host salary policy levels failed: %v", err)
	}
	for _, level := range policy.Levels {
		if level.Status == "" {
			level.Status = "active"
		}
		if _, err := r.schema.DB.ExecContext(context.Background(), `
			INSERT INTO host_agency_salary_policy_levels (
				app_code, policy_id, policy_version, level_no, required_diamonds, host_salary_usd_minor,
				host_coin_reward, agency_salary_usd_minor, status, sort_order, created_at_ms, updated_at_ms
			) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
			appCode, policy.PolicyID, policy.PolicyVersion, level.LevelNo, level.RequiredDiamonds, level.HostSalaryUSDMinor,
			level.HostCoinReward, level.AgencySalaryUSDMinor, level.Status, level.SortOrder, nowMs, nowMs,
		); err != nil {
			r.t.Fatalf("seed host salary policy level %d failed: %v", level.LevelNo, err)
		}
	}
	// 测试 helper 同时建立周期绑定，模拟后台一次完整发布；生产发布路径不允许 ON DUPLICATE 覆盖。
	if _, err := r.schema.DB.ExecContext(context.Background(), `
		INSERT INTO host_salary_policy_cycle_bindings (
			app_code, region_id, cycle_key, policy_id, policy_version, created_at_ms
		) VALUES (?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE
			policy_id = VALUES(policy_id), policy_version = VALUES(policy_version)`,
		appCode, policy.RegionID, policy.CycleKey, policy.PolicyID, policy.PolicyVersion, nowMs,
	); err != nil {
		r.t.Fatalf("seed host salary policy cycle binding failed: %v", err)
	}
}

func initDBPath(t testing.TB) string {
	t.Helper()

	return mysqlschema.InitDBPath(t,
		mysqlschema.CallerFile(t, 1),
		"..", "..", "..", "deploy", "mysql", "initdb", "001_wallet_service.sql",
	)
}

func validateTableName(table string) string {
	switch table {
	case "wallet_transactions", "wallet_entries", "wallet_outbox", "wallet_accounts", "wallet_recharge_records", "payment_orders",
		"wallet_user_recharge_stats", "external_recharge_orders", "third_party_payment_methods",
		"host_period_diamond_accounts", "host_period_diamond_entries", "point_diamond_gift_income_entries", "host_agency_salary_policies",
		"host_agency_salary_policy_levels", "host_salary_settlement_progress", "host_salary_settlement_records",
		"coin_seller_stock_records", "gift_diamond_ratio_configs", "wallet_diamond_exchange_rules", "agency_point_share_entries",
		"wallet_policy_instances",
		"point_withdrawal_policy_counters", "point_withdrawal_limit_reservations",
		"vip_program_configs", "vip_levels", "vip_level_benefits", "user_vip_memberships", "user_vip_settings", "vip_purchase_orders", "user_vip_history", "user_vip_trial_cards", "vip_command_locks", "vip_daily_coin_rebate_runs", "vip_daily_coin_rebates",
		"resources", "resource_groups", "resource_group_items", "resource_shop_items", "resource_shop_purchase_orders", "gift_type_configs", "gift_configs", "gift_config_regions", "wallet_gift_prices", "user_gift_wall", "wallet_projection_events",
		"resource_grants", "resource_grant_items", "user_resource_entitlements", "user_resource_equipment":
		return table
	default:
		panic(fmt.Sprintf("unsupported wallet test table %q", table))
	}
}
