package financeorder

import (
	"context"
	"strings"
	"testing"

	"hyapp-admin-server/internal/model"
	"hyapp-admin-server/internal/modules/shared"
	walletv1 "hyapp.local/api/proto/wallet/v1"
)

func TestNormalizeCreateOrderRequiresExplicitUSDTChain(t *testing.T) {
	_, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{
		AppCode:         "lalu",
		TargetUserID:    "10001",
		USDAmount:       12.34,
		ProviderCode:    "usdt",
		ExternalOrderNo: "0xabc",
	})
	if err == nil || !strings.Contains(err.Error(), "必须选择链") {
		t.Fatalf("expected explicit usdt chain error, got %v", err)
	}

	input, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{
		AppCode:         "lalu",
		TargetUserID:    "10001",
		USDAmount:       12.34,
		ProviderCode:    "usdt",
		Chain:           "bsc",
		ExternalOrderNo: "0xabc",
	})
	if err != nil {
		t.Fatalf("normalize usdt order failed: %v", err)
	}
	if input.ProviderCode != "usdt" || input.Chain != "BSC" || input.USDMinorAmount != 1234 {
		t.Fatalf("normalized input mismatch: %+v", input)
	}
}

func TestNormalizeCreateOrderSupportsExplicitBinanceOffchain(t *testing.T) {
	input, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{
		AppCode:         "aslan",
		TargetUserID:    "4337389",
		USDAmount:       300.14,
		ProviderCode:    "usdt",
		Chain:           "binance_offchain",
		ExternalOrderNo: "391252658053",
	})
	if err != nil {
		t.Fatalf("normalize explicit Binance Off-chain failed: %v", err)
	}
	if input.ProviderCode != "usdt" || input.Chain != "OFFCHAIN" || input.USDMinorAmount != 30014 || receiptVerificationProviderCode(normalizedReceiptVerificationInput{ProviderCode: input.ProviderCode, Chain: input.Chain}) != "usdt_offchain" {
		t.Fatalf("explicit Binance Off-chain mapping mismatch: %+v", input)
	}
}

func TestNormalizeCreateOrderRequiresProviderCurrencyAmount(t *testing.T) {
	for _, providerCode := range []string{"mifapay", "v5pay"} {
		_, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{
			AppCode:         "lalu",
			TargetUserID:    "10001",
			USDAmount:       12.34,
			ProviderCode:    providerCode,
			ExternalOrderNo: providerCode + "-order",
		})
		if err == nil || !strings.Contains(err.Error(), "三方币种") {
			t.Fatalf("expected %s missing currency error, got %v", providerCode, err)
		}

		input, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{
			AppCode:              "lalu",
			TargetUserID:         "10001",
			USDAmount:            12.34,
			ProviderCode:         providerCode,
			ExternalOrderNo:      providerCode + "-order",
			ProviderCurrencyCode: " php ",
			ProviderAmountMinor:  123400,
			ProviderPayType:      "GCASH",
		})
		if err != nil {
			t.Fatalf("normalize %s with provider currency amount failed: %v", providerCode, err)
		}
		if input.ProviderCurrencyCode != "PHP" || input.ProviderAmountMinor != 123400 || input.ProviderPayType != "GCASH" {
			t.Fatalf("provider currency amount should be normalized for %s: %+v", providerCode, input)
		}
	}
}

func TestNormalizeReceiptVerificationCarriesProviderCurrencyAmount(t *testing.T) {
	input, err := normalizeReceiptVerificationRequest(verifyCoinSellerRechargeReceiptRequest{
		AppCode:              "lalu",
		USDAmount:            10,
		ProviderCode:         "v5pay",
		ExternalOrderNo:      "V5-001",
		ProviderCountryCode:  " ph ",
		ProviderCurrencyCode: " php ",
		ProviderAmountMinor:  560000,
		ProviderPayType:      "GCASH",
		OrderDate:            "2026-07-09",
	})
	if err != nil {
		t.Fatalf("normalize receipt verification failed: %v", err)
	}
	if input.USDMinorAmount != 1000 || input.ProviderCountryCode != "PH" || input.ProviderCurrencyCode != "PHP" || input.ProviderAmountMinor != 560000 || input.ProviderPayType != "GCASH" {
		t.Fatalf("provider verification input mismatch: %+v", input)
	}
	if input.OrderDateMS <= 0 {
		t.Fatalf("order date should be normalized into millis: %+v", input)
	}

	_, err = normalizeReceiptVerificationRequest(verifyCoinSellerRechargeReceiptRequest{
		AppCode:         "lalu",
		ProviderCode:    "mifapay",
		ExternalOrderNo: "MIFA-001",
	})
	if err == nil || !strings.Contains(err.Error(), "三方币种") {
		t.Fatalf("expected missing provider currency error, got %v", err)
	}
}

func TestNormalizeCreateOrderBusinessTimeDefaultsToNowAndRejectsFuture(t *testing.T) {
	const nowMS = int64(1_783_900_000_000)
	if got, err := normalizeCreateOrderBusinessTime(0, nowMS); err != nil || got != nowMS {
		t.Fatalf("missing order time should default to now: got=%d err=%v", got, err)
	}
	if got, err := normalizeCreateOrderBusinessTime(nowMS-86_400_000, nowMS); err != nil || got != nowMS-86_400_000 {
		t.Fatalf("past business time should be preserved: got=%d err=%v", got, err)
	}
	if _, err := normalizeCreateOrderBusinessTime(nowMS+1, nowMS); err == nil || !strings.Contains(err.Error(), "不能晚于") {
		t.Fatalf("future business time should be rejected, got %v", err)
	}
}

func TestNormalizeCreateOrderLeavesCoinAmountForServerQuote(t *testing.T) {
	input, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{
		AppCode:              "lalu",
		TargetUserID:         "10001",
		USDAmount:            12.34,
		ProviderCode:         "mifapay",
		ExternalOrderNo:      "MIFA-001",
		ProviderCurrencyCode: "PHP",
		ProviderAmountMinor:  1234,
		Remark:               " 补单，不发金币 ",
	})
	if err != nil {
		t.Fatalf("create order input should be accepted before server quote: %v", err)
	}
	if input.CoinAmount != 0 || input.USDMinorAmount != 1234 || input.Remark != "补单，不发金币" {
		t.Fatalf("normalized order should not contain a client-controlled coin amount: %+v", input)
	}
}

func TestNormalizeCreateOrderCarriesExplicitMakeupState(t *testing.T) {
	input, err := normalizeCreateOrderRequest(createCoinSellerRechargeOrderRequest{
		AppCode:              "lalu",
		TargetUserID:         "10001",
		USDAmount:            12.34,
		IsMakeup:             true,
		ProviderCode:         "mifapay",
		ExternalOrderNo:      "MIFA-MAKEUP-001",
		ProviderCurrencyCode: "PHP",
		ProviderAmountMinor:  1234,
	})
	if err != nil {
		t.Fatalf("normalize makeup order failed: %v", err)
	}
	if !input.IsMakeup || input.CoinAmount != 0 {
		t.Fatalf("makeup state must force the later server branch to keep zero coins: %+v", input)
	}
}

func TestCreateOrderMakeupBypassesExchangeRateAndForcesZeroCoins(t *testing.T) {
	service := NewService(nil, nil, nil)
	coinAmount, err := service.createOrderCoinAmount(normalizedCreateOrderInput{
		AppCode:        "lalu",
		TargetUserID:   "123456",
		USDMinorAmount: 15000,
		IsMakeup:       true,
	}, coinSellerTarget{UserID: 9988, DisplayUserID: "123456"})
	if err != nil || coinAmount != 0 {
		t.Fatalf("makeup must not require an exchange-rate config and must force zero coins: amount=%d err=%v", coinAmount, err)
	}
}

func TestCoinSellerRechargeQuotePrefersWhitelistOverAmountTier(t *testing.T) {
	policy := coinSellerRechargeExchangeRatePolicy{
		Tiers: []coinSellerRechargeExchangeRateTier{
			{MinUSDMinor: 10000, MaxUSDMinor: 20000, CoinsPerUSD: 80000},
			{MinUSDMinor: 20100, MaxUSDMinor: 30000, CoinsPerUSD: 90000},
		},
		Whitelist: []coinSellerRechargeExchangeRateWhitelist{{UserID: "123456", CoinsPerUSD: 100000}},
	}

	quote, err := calculateCoinSellerRechargeQuote(policy, "123456", coinSellerTarget{UserID: 9988, DisplayUserID: "123456"}, 15000)
	if err != nil {
		t.Fatalf("calculate whitelist quote failed: %v", err)
	}
	if quote.RateSource != "whitelist" || quote.CoinsPerUSD != 100000 || quote.CoinAmount != 15000000 || quote.MatchedUserID != "123456" {
		t.Fatalf("whitelist quote mismatch: %+v", quote)
	}

	tierQuote, err := calculateCoinSellerRechargeQuote(policy, "654321", coinSellerTarget{UserID: 7766, DisplayUserID: "654321"}, 25000)
	if err != nil {
		t.Fatalf("calculate tier quote failed: %v", err)
	}
	if tierQuote.RateSource != "tier" || tierQuote.CoinsPerUSD != 90000 || tierQuote.CoinAmount != 22500000 || tierQuote.MinUSDAmount != "201.00" || tierQuote.MaxUSDAmount != "300.00" {
		t.Fatalf("tier quote mismatch: %+v", tierQuote)
	}
}

func TestNormalizeCoinSellerRechargeExchangeRateRejectsOverlapsAndDuplicateWhitelist(t *testing.T) {
	_, err := normalizeExchangeRatePolicyRequest(coinSellerRechargeExchangeRatePolicyRequest{Tiers: []coinSellerRechargeExchangeRateTierRequest{
		{MinUSDAmount: 100, MaxUSDAmount: 200, CoinsPerUSD: 80000},
		{MinUSDAmount: 200, MaxUSDAmount: 300, CoinsPerUSD: 90000},
	}})
	if err == nil || !strings.Contains(err.Error(), "不能重叠") {
		t.Fatalf("expected overlap validation error, got %v", err)
	}

	_, err = normalizeExchangeRatePolicyRequest(coinSellerRechargeExchangeRatePolicyRequest{
		Tiers: []coinSellerRechargeExchangeRateTierRequest{{MinUSDAmount: 1, MaxUSDAmount: 10, CoinsPerUSD: 80000}},
		Whitelist: []coinSellerRechargeExchangeRateWhitelistRequest{
			{UserID: "123456", CoinsPerUSD: 100000},
			{UserID: "123456", CoinsPerUSD: 110000},
		},
	})
	if err == nil || !strings.Contains(err.Error(), "重复") {
		t.Fatalf("expected duplicate whitelist validation error, got %v", err)
	}
}

func TestExecuteGrantSkipsWalletAndLegacyForZeroCoinMakeupOrder(t *testing.T) {
	service := NewService(nil, nil, nil)
	result, err := service.executeGrant(context.Background(), shared.Actor{UserID: 7}, model.CoinSellerRechargeOrder{
		ID:           42,
		AppCode:      "aslan",
		TargetUserID: 10001,
		CoinAmount:   0,
	}, "req-1")
	if err != nil {
		t.Fatalf("zero coin makeup order should not require wallet or legacy writer: %v", err)
	}
	if result.CommandID != "admin-coin-seller-recharge:42" || result.TransactionID != "" || result.AmountDelta != 0 {
		t.Fatalf("zero coin grant result mismatch: %+v", result)
	}
}

func TestExecuteGrantUsesLegacyWriterForAslan(t *testing.T) {
	writer := &fakeLegacyCoinSellerRechargeWriter{
		appCode: "aslan",
		grantResult: legacyRechargeGrantResult{
			TransactionID: "legacy-tx-1",
			BalanceAfter:  6200,
		},
	}
	service := NewService(nil, nil, nil, WithLegacyCoinSellerRechargeWriters(writer))
	result, err := service.executeGrant(context.Background(), shared.Actor{UserID: 7}, model.CoinSellerRechargeOrder{
		ID:              99,
		AppCode:         "aslan",
		TargetUserID:    10001,
		ExternalOrderNo: "V5-001",
		ProviderCode:    "v5pay",
		USDAmount:       "10.00",
		CoinAmount:      1200,
	}, "req-1")
	if err != nil {
		t.Fatalf("aslan legacy grant failed: %v", err)
	}
	if writer.grantCalls != 1 || writer.grantInput.UserID != 10001 || writer.grantInput.ExternalOrderNo != "V5-001" || writer.grantInput.OperatorUserID != 7 {
		t.Fatalf("legacy writer grant input mismatch: calls=%d input=%+v", writer.grantCalls, writer.grantInput)
	}
	if result.CommandID != "admin-coin-seller-recharge:99" || result.TransactionID != "legacy-tx-1" || result.AmountDelta != 1200 || result.BalanceAfter != 6200 {
		t.Fatalf("legacy grant result mismatch: %+v", result)
	}
}

func TestExecuteGrantRequiresLegacyWriterForYumi(t *testing.T) {
	service := NewService(nil, nil, nil)
	_, err := service.executeGrant(context.Background(), shared.Actor{UserID: 7}, model.CoinSellerRechargeOrder{
		ID:              100,
		AppCode:         "yumi",
		TargetUserID:    20002,
		ExternalOrderNo: "MIFA-001",
		ProviderCode:    "mifapay",
		USDAmount:       "12.34",
		CoinAmount:      1000,
	}, "req-1")
	if err == nil || !strings.Contains(err.Error(), "legacy coin seller recharge writer is not configured") {
		t.Fatalf("expected yumi legacy writer configuration error, got %v", err)
	}
}

func TestResolveCoinSellerTargetUsesLegacyWriterForAslan(t *testing.T) {
	writer := &fakeLegacyCoinSellerRechargeWriter{
		appCode: "aslan",
		resolveResult: coinSellerTarget{
			UserID:        10001,
			DisplayUserID: "agent001",
			CountryID:     63,
			CountryCode:   "PH",
		},
	}
	var resolvedApp, resolvedCountry string
	service := NewService(nil, nil, nil,
		WithLegacyCoinSellerRechargeWriters(writer),
		WithLegacyCoinSellerRegionResolver(func(_ context.Context, appCode string, countryCode string) (int64, bool, error) {
			resolvedApp, resolvedCountry = appCode, countryCode
			return 2049039964177891329, true, nil
		}))
	target, err := service.resolveCoinSellerTarget(context.Background(), "req-1", "aslan", "agent001")
	if err != nil {
		t.Fatalf("resolve aslan legacy target failed: %v", err)
	}
	if writer.resolveKeyword != "agent001" || target.UserID != 10001 || target.DisplayUserID != "agent001" || target.CountryID != 63 {
		t.Fatalf("legacy resolve mismatch: keyword=%q target=%+v", writer.resolveKeyword, target)
	}
	if resolvedApp != "aslan" || resolvedCountry != "PH" || target.RegionID != 2049039964177891329 {
		t.Fatalf("legacy region attribution mismatch: app=%q country=%q target=%+v", resolvedApp, resolvedCountry, target)
	}
}

func TestResolveCoinSellerTargetLegacyRequiresRegionAttribution(t *testing.T) {
	writer := &fakeLegacyCoinSellerRechargeWriter{
		appCode: "aslan",
		resolveResult: coinSellerTarget{
			UserID:        10001,
			DisplayUserID: "agent001",
			CountryID:     63,
			CountryCode:   "PH",
		},
	}

	// 国家未划入任何区域：创建必须失败，否则订单以 target_region_id=0 落库后从区域视图消失。
	service := NewService(nil, nil, nil,
		WithLegacyCoinSellerRechargeWriters(writer),
		WithLegacyCoinSellerRegionResolver(func(_ context.Context, _ string, _ string) (int64, bool, error) {
			return 0, false, nil
		}))
	if _, err := service.resolveCoinSellerTarget(context.Background(), "req-1", "aslan", "agent001"); err == nil || !strings.Contains(err.Error(), "未划入任何区域") {
		t.Fatalf("expected unmapped country error, got %v", err)
	}

	// 用户没有国家码：同样必须失败。
	writer.resolveResult.CountryCode = ""
	if _, err := service.resolveCoinSellerTarget(context.Background(), "req-1", "aslan", "agent001"); err == nil || !strings.Contains(err.Error(), "未配置国家") {
		t.Fatalf("expected missing country error, got %v", err)
	}

	// 未接入区域解析器：显式报配置错误而不是静默回到 region 0。
	writer.resolveResult.CountryCode = "PH"
	service = NewService(nil, nil, nil, WithLegacyCoinSellerRechargeWriters(writer))
	if _, err := service.resolveCoinSellerTarget(context.Background(), "req-1", "aslan", "agent001"); err == nil || !strings.Contains(err.Error(), "region resolver is not configured") {
		t.Fatalf("expected resolver configuration error, got %v", err)
	}
}

func TestVerifyReceiptMatchesOrderAmounts(t *testing.T) {
	order := model.CoinSellerRechargeOrder{
		ExternalOrderNo:      "merchant-1",
		USDMinorAmount:       1234,
		ProviderCurrencyCode: "USD",
		ProviderAmountMinor:  0,
		ProviderPayloadJSON:  "",
		TargetDisplayUserID:  "10001",
		TargetCountryID:      1,
		TargetRegionID:       2,
		ProviderCountryCode:  "SA",
		ProviderPayType:      "CARD",
		ProviderOrderID:      "merchant-1",
		ProviderStatus:       "paid",
		WalletTransactionID:  "",
		WalletAssetType:      "",
		WalletAmountDelta:    0,
		WalletBalanceAfter:   0,
		OperatorName:         "ops",
		VerifiedByName:       "",
		GrantedByName:        "",
		ReceiveAddress:       "",
		FailureReason:        "",
		WalletCommandID:      "",
		TargetUserID:         10001,
		ProviderCode:         "mifapay",
		Chain:                "",
		USDAmount:            "12.34",
		CoinAmount:           1200,
		Status:               model.CoinSellerRechargeOrderStatusCreated,
		VerifyStatus:         model.CoinSellerRechargeVerifyStatusPending,
		GrantStatus:          model.CoinSellerRechargeGrantStatusPending,
		OperatorUserID:       7,
		CreatedAtMS:          1,
		UpdatedAtMS:          1,
	}
	if err := verifyReceiptMatchesOrder(order, &walletv1.VerifyCoinSellerRechargeReceiptResponse{
		ExternalOrderNo:     "merchant-1",
		CurrencyCode:        "USD",
		ProviderAmountMinor: 1234,
	}); err != nil {
		t.Fatalf("expected USD amount to match: %v", err)
	}
	if err := verifyReceiptMatchesOrder(order, &walletv1.VerifyCoinSellerRechargeReceiptResponse{
		ExternalOrderNo:     "merchant-1",
		CurrencyCode:        "USD",
		ProviderAmountMinor: 1200,
	}); err == nil {
		t.Fatalf("expected USD amount mismatch")
	}
}

func TestVerifyReceiptMatchesNonUSDProviderAmount(t *testing.T) {
	order := model.CoinSellerRechargeOrder{
		ExternalOrderNo:      "v5-1",
		USDMinorAmount:       1000,
		ProviderCurrencyCode: "IDR",
		ProviderAmountMinor:  15000000,
		ProviderCode:         "v5pay",
		TargetDisplayUserID:  "10001",
		TargetUserID:         10001,
		USDAmount:            "10.00",
		CoinAmount:           1000,
		Status:               model.CoinSellerRechargeOrderStatusCreated,
		VerifyStatus:         model.CoinSellerRechargeVerifyStatusPending,
		GrantStatus:          model.CoinSellerRechargeGrantStatusPending,
		OperatorUserID:       7,
	}
	if err := verifyReceiptMatchesOrder(order, &walletv1.VerifyCoinSellerRechargeReceiptResponse{
		ExternalOrderNo:     "v5-1",
		CurrencyCode:        "IDR",
		ProviderAmountMinor: 15000000,
	}); err != nil {
		t.Fatalf("expected provider amount to match: %v", err)
	}
	order.ProviderAmountMinor = 0
	if err := verifyReceiptMatchesOrder(order, &walletv1.VerifyCoinSellerRechargeReceiptResponse{
		ExternalOrderNo:     "v5-1",
		CurrencyCode:        "IDR",
		ProviderAmountMinor: 15000000,
	}); err != nil {
		t.Fatalf("non-USD provider amount should be an audit snapshot when operator did not fill local minor amount: %v", err)
	}
}

func TestVerifyReceiptMatchesInputRequiresUSDExactMatch(t *testing.T) {
	input := normalizedCreateOrderInput{
		ExternalOrderNo: "usdt-tx",
		USDMinorAmount:  1234,
		ProviderCode:    "usdt",
		Chain:           "TRON",
	}
	if err := verifyReceiptMatchesInput(input, coinSellerRechargeReceiptVerificationDTO{
		ExternalOrderNo:     "usdt-tx",
		CurrencyCode:        "USDT",
		ProviderAmountMinor: 1234,
	}); err != nil {
		t.Fatalf("expected usdt amount to match: %v", err)
	}
	if err := verifyReceiptMatchesInput(input, coinSellerRechargeReceiptVerificationDTO{
		ExternalOrderNo:     "usdt-tx",
		CurrencyCode:        "USDT",
		ProviderAmountMinor: 1200,
	}); err == nil {
		t.Fatalf("expected usdt amount mismatch")
	}
}

func TestReceiptProviderCodeForUSDTChains(t *testing.T) {
	if got := receiptProviderCode(model.CoinSellerRechargeOrder{ProviderCode: "usdt", Chain: "TRON"}); got != "usdt_trc20" {
		t.Fatalf("tron provider code mismatch: %s", got)
	}
	if got := receiptProviderCode(model.CoinSellerRechargeOrder{ProviderCode: "usdt", Chain: "BSC"}); got != "usdt_bep20" {
		t.Fatalf("bsc provider code mismatch: %s", got)
	}
}

type fakeLegacyCoinSellerRechargeWriter struct {
	appCode        string
	resolveKeyword string
	resolveResult  coinSellerTarget
	resolveErr     error
	grantCalls     int
	grantInput     legacyRechargeGrantInput
	grantResult    legacyRechargeGrantResult
	grantErr       error
}

func (f *fakeLegacyCoinSellerRechargeWriter) AppCode() string {
	return f.appCode
}

func (f *fakeLegacyCoinSellerRechargeWriter) Close() error {
	return nil
}

func (f *fakeLegacyCoinSellerRechargeWriter) ResolveCoinSeller(_ context.Context, keyword string) (coinSellerTarget, error) {
	f.resolveKeyword = keyword
	return f.resolveResult, f.resolveErr
}

func (f *fakeLegacyCoinSellerRechargeWriter) GrantCoinSellerRecharge(_ context.Context, input legacyRechargeGrantInput) (legacyRechargeGrantResult, error) {
	f.grantCalls++
	f.grantInput = input
	return f.grantResult, f.grantErr
}
