package mysql

import (
	"context"
	"regexp"
	"testing"
	"time"

	"hyapp/pkg/appcode"
	"hyapp/services/wallet-service/internal/domain/ledger"
	resourcedomain "hyapp/services/wallet-service/internal/domain/resource"

	"github.com/DATA-DOG/go-sqlmock"
	mysqlDriver "github.com/go-sql-driver/mysql"
)

func TestEnsureGiftDiamondRatioSchemaDoesNotSeedAnyApp(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sqlmock: %v", err)
	}
	defer db.Close()
	mock.ExpectExec("CREATE TABLE IF NOT EXISTS gift_diamond_ratio_configs").WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectExec("ALTER TABLE gift_diamond_ratio_configs").WillReturnError(&mysqlDriver.MySQLError{Number: 1060, Message: "Duplicate column name"})
	if err := ensureGiftDiamondRatioSchema(context.Background(), db); err != nil {
		t.Fatalf("ensure gift ratio schema: %v", err)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("wallet startup must not issue a gift multiplier INSERT: %v", err)
	}
}

func TestOrderedWalletAccountLockRequestsUsesSameOrderForGiftAndExchange(t *testing.T) {
	giftOrder, err := orderedWalletAccountLockRequests([]walletAccountLockRequest{
		{UserID: 42, AssetType: ledger.AssetCoin, CreateIfMissing: false},
		{UserID: 42, AssetType: ledger.AssetPointDiamond, CreateIfMissing: true},
	})
	if err != nil {
		t.Fatalf("order gift locks: %v", err)
	}
	exchangeOrder, err := orderedWalletAccountLockRequests([]walletAccountLockRequest{
		{UserID: 42, AssetType: ledger.AssetPointDiamond, CreateIfMissing: false},
		{UserID: 42, AssetType: ledger.AssetCoin, CreateIfMissing: true},
	})
	if err != nil {
		t.Fatalf("order exchange locks: %v", err)
	}
	if len(giftOrder) != 2 || len(exchangeOrder) != 2 || giftOrder[0].AssetType != ledger.AssetCoin || giftOrder[1].AssetType != ledger.AssetPointDiamond || exchangeOrder[0].AssetType != ledger.AssetCoin || exchangeOrder[1].AssetType != ledger.AssetPointDiamond {
		t.Fatalf("opposite money flows must lock in one canonical order: gift=%+v exchange=%+v", giftOrder, exchangeOrder)
	}
	sellerOrder, err := orderedWalletAccountLockRequests([]walletAccountLockRequest{
		{UserID: 900, AssetType: ledger.AssetPointDiamond, CreateIfMissing: false},
		{UserID: 1, AssetType: ledger.AssetCoinSellerCoin, CreateIfMissing: true},
	})
	if err != nil {
		t.Fatalf("order coin seller locks: %v", err)
	}
	if len(sellerOrder) != 2 || sellerOrder[0].AssetType != ledger.AssetCoinSellerCoin || sellerOrder[0].UserID != 1 || sellerOrder[1].AssetType != ledger.AssetPointDiamond || sellerOrder[1].UserID != 900 {
		t.Fatalf("coin seller transfer must use canonical asset/user lock order: %+v", sellerOrder)
	}
}

func TestResolveGiftReturnCoinRatioFailsWithoutPersistedConfiguration(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sqlmock: %v", err)
	}
	defer db.Close()
	mock.ExpectBegin()
	tx, err := db.Begin()
	if err != nil {
		t.Fatalf("begin: %v", err)
	}
	defer tx.Rollback()
	query := regexp.QuoteMeta(`
		SELECT CAST(coin_return_ratio_percent AS CHAR), CAST(ROUND(coin_return_ratio_percent * 10000) AS SIGNED)
		FROM gift_diamond_ratio_configs
		WHERE app_code = ? AND region_id = ? AND gift_type_code = ? AND status = 'active'
		LIMIT 1`)
	mock.ExpectQuery(query).WithArgs("fami", int64(25), "normal").WillReturnRows(sqlmock.NewRows([]string{"percent", "ppm"}))
	mock.ExpectQuery(query).WithArgs("fami", int64(0), "normal").WillReturnRows(sqlmock.NewRows([]string{"percent", "ppm"}))
	repository := &Repository{db: db}
	if _, _, err := repository.resolveGiftReturnCoinRatio(appcode.WithContext(context.Background(), "fami"), tx, "fami", 25, "normal"); err == nil {
		t.Fatal("missing persisted return-coin ratio must fail closed")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet SQL expectations: %v", err)
	}
}

func TestNormalizeGiftConfigListQueryAllowsPanelPageSize(t *testing.T) {
	query := normalizeGiftConfigListQuery(resourcedomain.ListGiftConfigsQuery{
		Page:     1,
		PageSize: 500,
	})
	if query.PageSize != 500 {
		t.Fatalf("gift panel list page size must allow 500, got %d", query.PageSize)
	}

	query = normalizeGiftConfigListQuery(resourcedomain.ListGiftConfigsQuery{
		Page:     1,
		PageSize: 501,
	})
	if query.PageSize != 500 {
		t.Fatalf("gift list page size must be capped at 500, got %d", query.PageSize)
	}
}

func TestResolveHostPolicyDiamondCoefficientUsesPublishedCycleSnapshot(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sql mock: %v", err)
	}
	defer db.Close()
	mock.ExpectBegin()
	tx, err := db.Begin()
	if err != nil {
		t.Fatalf("begin: %v", err)
	}
	defer tx.Rollback()

	mock.ExpectQuery(regexp.QuoteMeta(`
			SELECT p.policy_id, p.policy_version, p.policy_type,
			       CAST(p.gift_coin_to_diamond_ratio AS CHAR),
			       CAST(ROUND(p.gift_coin_to_diamond_ratio * 1000000) AS SIGNED),
			       p.point_diamonds_per_usd, p.coins_per_usd, p.minimum_withdraw_usd_minor, p.withdraw_fee_bps,
			       p.agency_point_share_bps
			FROM host_salary_policy_cycle_bindings binding
			JOIN host_agency_salary_policies p
			  ON p.app_code = binding.app_code
			 AND p.policy_id = binding.policy_id
			 AND p.policy_version = binding.policy_version
			WHERE binding.app_code = ?
			  AND binding.region_id = ?
			  AND binding.cycle_key = ?
			  AND p.status = 'active'
			LIMIT 1`)).
		WithArgs("fami", int64(26), "2026-07").
		WillReturnRows(sqlmock.NewRows([]string{"policy_id", "policy_version", "policy_type", "coefficient", "coefficient_ppm", "points_per_usd", "coins_per_usd", "minimum_usd_minor", "fee_bps", "agency_bps"}).
			AddRow(uint64(81), uint64(7), "POINT_DIAMOND", "1.250000", int64(1_250_000), int64(300000), int64(80000), int64(5000), int32(100), int32(2000)))

	repository := &Repository{db: db}
	snapshot, found, err := repository.resolveHostPolicyDiamondCoefficient(appcode.WithContext(context.Background(), "fami"), tx, 26, "2026-07")
	if err != nil {
		t.Fatalf("resolve Host policy diamond coefficient: %v", err)
	}
	if !found || snapshot.PolicyID != 81 || snapshot.PolicyVersion != 7 || snapshot.PolicyType != "POINT_DIAMOND" || snapshot.PPM != 1_250_000 || snapshot.Value != "1.250000" || snapshot.AgencyPointShareBPS != 2000 {
		t.Fatalf("published Host policy snapshot mismatch: %+v found=%v", snapshot, found)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet sql expectations: %v", err)
	}
}

func TestGiftDiamondAmountWithPolicyCoefficientFloorsOnce(t *testing.T) {
	tests := []struct {
		name                 string
		chargeAmount         int64
		giftTypeRatioPPM     int64
		policyCoefficientPPM int64
		want                 int64
	}{
		{name: "normal neutral", chargeAmount: 100, giftTypeRatioPPM: 1_000_000, policyCoefficientPPM: 1_000_000, want: 100},
		{name: "lucky neutral", chargeAmount: 100, giftTypeRatioPPM: 100_000, policyCoefficientPPM: 1_000_000, want: 10},
		{name: "super lucky neutral", chargeAmount: 100, giftTypeRatioPPM: 10_000, policyCoefficientPPM: 1_000_000, want: 1},
		{name: "lucky with Host coefficient", chargeAmount: 100, giftTypeRatioPPM: 100_000, policyCoefficientPPM: 1_250_000, want: 12},
		{name: "single final floor", chargeAmount: 1, giftTypeRatioPPM: 500_000, policyCoefficientPPM: 2_000_000, want: 1},
		{name: "disabled gift type", chargeAmount: 100, giftTypeRatioPPM: 0, policyCoefficientPPM: 1_000_000, want: 0},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := giftDiamondAmountWithPolicyCoefficient(tt.chargeAmount, tt.giftTypeRatioPPM, tt.policyCoefficientPPM)
			if err != nil {
				t.Fatalf("calculate period diamonds: %v", err)
			}
			if got != tt.want {
				t.Fatalf("period diamonds mismatch: got %d want %d", got, tt.want)
			}
		})
	}
}

func TestGiftDiamondAmountWithPolicyCoefficientRejectsInvalidOrOverflowingValues(t *testing.T) {
	if _, err := giftDiamondAmountWithPolicyCoefficient(100, 1_000_000, 0); err == nil {
		t.Fatal("zero Host policy coefficient must be rejected")
	}
	if _, err := giftDiamondAmountWithPolicyCoefficient(9_223_372_036_854_775_807, 1_000_000, 2_000_000); err == nil {
		t.Fatal("period diamond result outside BIGINT must be rejected")
	}
}

func TestCalculatePointDiamondCoinSellerAmountsAppliesFeeBeforeSellerCredit(t *testing.T) {
	points, feeUSDMinor, netUSDMinor, sellerCoins, err := calculatePointDiamondCoinSellerAmounts(10_001, 300_000, 80_000, 333)
	if err != nil {
		t.Fatalf("calculate point diamond seller withdrawal: %v", err)
	}
	// 扣积分按 gross；手续费先在美元分上 floor；币商库存只按 net 计算。
	if points != 30_003_000 || feeUSDMinor != 333 || netUSDMinor != 9_668 || sellerCoins != 7_734_400 {
		t.Fatalf("seller withdrawal snapshot mismatch: points=%d fee=%d net=%d coins=%d", points, feeUSDMinor, netUSDMinor, sellerCoins)
	}
}

func TestPointWithdrawalActionAvailabilityUsesCurrentCounterAndAllowedDay(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("create sql mock: %v", err)
	}
	defer db.Close()
	mock.ExpectBegin()
	tx, err := db.Begin()
	if err != nil {
		t.Fatalf("begin: %v", err)
	}
	defer tx.Rollback()
	now := time.Date(2026, 7, 22, 8, 0, 0, 0, time.UTC)
	mock.ExpectQuery(regexp.QuoteMeta(`
		SELECT withdrawal_count
		FROM point_withdrawal_policy_counters
		WHERE app_code = ? AND source_asset_type = ? AND user_id = ?
		  AND channel = ? AND period = ? AND period_key = ?`)).
		WithArgs("fami", ledger.AssetPointDiamond, int64(42), pointWithdrawalChannelCoinSeller, withdrawalLimitPeriodMonth, "2026-07").
		WillReturnRows(sqlmock.NewRows([]string{"withdrawal_count"}).AddRow(int64(2)))
	repo := &Repository{db: db}
	ctx := appcode.WithContext(context.Background(), "fami")
	coinSeller, err := repo.pointWithdrawalActionAvailability(ctx, tx, 42, ledger.AssetPointDiamond, pointWithdrawalChannelCoinSeller, hostPolicyWithdrawalLimit{
		Period: withdrawalLimitPeriodMonth, PeriodKey: "2026-07", Limit: 2,
	}, now)
	if err != nil {
		t.Fatalf("coin seller availability: %v", err)
	}
	if coinSeller.Allowed || coinSeller.UsedCount != 2 || coinSeller.RemainingCount != 0 || coinSeller.BlockReason != "limit_reached" {
		t.Fatalf("limit-reached capability mismatch: %+v", coinSeller)
	}
	platform, err := repo.pointWithdrawalActionAvailability(ctx, tx, 42, ledger.AssetPointDiamond, pointWithdrawalChannelPlatform, hostPolicyWithdrawalLimit{
		Period: withdrawalLimitPeriodMonth, PeriodKey: "2026-07", Limit: 0, AllowedDays: "1,15",
	}, now)
	if err != nil {
		t.Fatalf("platform availability: %v", err)
	}
	if platform.Allowed || platform.RemainingCount != -1 || platform.BlockReason != "date_not_allowed" {
		t.Fatalf("date capability mismatch: %+v", platform)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatalf("unmet SQL expectations: %v", err)
	}
}

func TestWalletTransactionEntryComponentsPreservesMergedHostAgencySplit(t *testing.T) {
	item := ledger.WalletTransaction{BizType: bizTypeGiftDebit}
	metadata := `{"target_user_id":101,"target_agency_owner_user_id":101,"host_base_amount":100,"agency_amount":20}`
	role, hostBase, agency := walletTransactionEntryComponents(101, item, metadata)
	if role != "host_gift_income_and_agency_share" || hostBase != 100 || agency != 20 {
		t.Fatalf("merged gift income components mismatch: role=%q host=%d agency=%d", role, hostBase, agency)
	}

	metadata = `{"target_user_id":101,"target_agency_owner_user_id":202,"host_base_amount":100,"agency_amount":20}`
	role, hostBase, agency = walletTransactionEntryComponents(101, item, metadata)
	if role != "host_gift_income" || hostBase != 100 || agency != 0 {
		t.Fatalf("Host-only gift income components mismatch: role=%q host=%d agency=%d", role, hostBase, agency)
	}
	role, hostBase, agency = walletTransactionEntryComponents(202, item, metadata)
	if role != "agency_gift_share" || hostBase != 0 || agency != 20 {
		t.Fatalf("Agency-only gift income components mismatch: role=%q host=%d agency=%d", role, hostBase, agency)
	}
}

func TestDirectGiftWalletHistoryUsesGiftPolicyAndRoleSnapshot(t *testing.T) {
	metadata := `{"target_user_id":101,"target_agency_owner_user_id":101,"host_base_amount":100,"agency_amount":20,"host_salary_policy_id":81,"host_salary_policy_version":7,"point_diamonds_per_usd":300000,"coins_per_usd":80000}`
	item := ledger.WalletTransaction{BizType: bizTypeDirectGiftDebit}
	role, hostBase, agency := walletTransactionEntryComponents(101, item, metadata)
	if role != "host_gift_income_and_agency_share" || hostBase != 100 || agency != 20 {
		t.Fatalf("direct gift role snapshot mismatch: role=%q host=%d agency=%d", role, hostBase, agency)
	}
	policyID, version, pointRatio, coinRatio, gross, fee, net, feeBPS := walletTransactionPolicySnapshot(bizTypeDirectGiftDebit, metadata)
	if policyID != 81 || version != 7 || pointRatio != 300000 || coinRatio != 80000 || gross != 0 || fee != 0 || net != 0 || feeBPS != 0 {
		t.Fatalf("direct gift policy snapshot mismatch: id=%d version=%d point=%d coin=%d gross=%d fee=%d net=%d fee_bps=%d", policyID, version, pointRatio, coinRatio, gross, fee, net, feeBPS)
	}
}
