package config

import (
	"crypto/rsa"
	"crypto/x509"
	"encoding/base64"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"gopkg.in/yaml.v3"
)

func TestLoadLocalEnablesOutboxMQ(t *testing.T) {
	cfg, err := Load("../../configs/config.yaml")
	if err != nil {
		t.Fatalf("Load local config failed: %v", err)
	}
	// 本地默认发布普通钱包事实和实时红包事实，链路和 Docker/testbox/线上保持一致。
	if !cfg.RocketMQ.Enabled || !cfg.RocketMQ.WalletOutbox.Enabled || !cfg.RocketMQ.RealtimeOutbox.Enabled {
		t.Fatalf("local config must enable wallet outbox MQ producers: %+v", cfg.RocketMQ)
	}
	if cfg.RocketMQ.WalletEventTagMode != WalletEventTagModeLegacy {
		t.Fatalf("Phase A config must publish legacy wallet tags: %q", cfg.RocketMQ.WalletEventTagMode)
	}
	if !cfg.OutboxWorker.Enabled || !cfg.RealtimeOutboxWorker.Enabled || !cfg.ProjectionWorker.Enabled {
		t.Fatalf("local config must enable wallet outbox workers: outbox=%+v realtime=%+v projection=%+v", cfg.OutboxWorker, cfg.RealtimeOutboxWorker, cfg.ProjectionWorker)
	}
	if !cfg.ExternalRechargeReconcileWorker.Enabled || cfg.ExternalRechargeReconcileWorker.BatchSize != 50 {
		t.Fatalf("local config must enable external recharge reconcile worker: %+v", cfg.ExternalRechargeReconcileWorker)
	}
	if cfg.OutboxWorker.Concurrency != 1 || cfg.RealtimeOutboxWorker.Concurrency != 2 {
		t.Fatalf("local outbox worker defaults mismatch: outbox=%+v realtime=%+v", cfg.OutboxWorker, cfg.RealtimeOutboxWorker)
	}
	if len(cfg.OutboxWorker.AppCodes) != 0 || len(cfg.RealtimeOutboxWorker.AppCodes) != 0 {
		t.Fatalf("empty app_codes must retain automatic discovery: outbox=%v realtime=%v", cfg.OutboxWorker.AppCodes, cfg.RealtimeOutboxWorker.AppCodes)
	}
	if cfg.BSCUSDT.Enabled || cfg.ExternalRecharge.USDTBEP20Enabled {
		t.Fatalf("bsc usdt receipt verification must stay disabled by default: bsc=%+v external=%+v", cfg.BSCUSDT, cfg.ExternalRecharge)
	}
}

func TestOutboxWorkerAppCodesNormalizeForStagedReplay(t *testing.T) {
	cfg := normalizeOutboxWorkerConfig(OutboxWorkerConfig{AppCodes: []string{" LALU ", "fami", "HUWAA", "fami"}}, Default().OutboxWorker)
	if got := strings.Join(cfg.AppCodes, ","); got != "lalu,fami,huwaa" {
		t.Fatalf("normalized outbox app_codes = %q", got)
	}
}

func TestDefaultWalletEventTagModeIsLegacy(t *testing.T) {
	if got := Default().RocketMQ.WalletEventTagMode; got != WalletEventTagModeLegacy {
		t.Fatalf("default wallet event tag mode = %q, want %q", got, WalletEventTagModeLegacy)
	}
}

func TestWalletEventTagModeMissingConfigDefaultsToLegacy(t *testing.T) {
	path := writeWalletConfig(t, "rocketmq:\n  enabled: false\n")
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load missing tag mode config failed: %v", err)
	}
	if cfg.RocketMQ.WalletEventTagMode != WalletEventTagModeLegacy {
		t.Fatalf("missing wallet_event_tag_mode = %q, want %q", cfg.RocketMQ.WalletEventTagMode, WalletEventTagModeLegacy)
	}
}

func TestWalletEventTagModeParsesExplicitPhaseB(t *testing.T) {
	path := writeWalletConfig(t, "rocketmq:\n  enabled: false\n  wallet_event_tag_mode: ' EVENT_TYPE '\n")
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("Load Phase B tag mode config failed: %v", err)
	}
	if cfg.RocketMQ.WalletEventTagMode != WalletEventTagModeEventType {
		t.Fatalf("wallet_event_tag_mode = %q, want %q", cfg.RocketMQ.WalletEventTagMode, WalletEventTagModeEventType)
	}
}

func TestWalletEventTagModeRejectsUnknownValue(t *testing.T) {
	path := writeWalletConfig(t, "rocketmq:\n  enabled: false\n  wallet_event_tag_mode: wildcard\n")
	if _, err := Load(path); err == nil || !strings.Contains(err.Error(), "wallet_event_tag_mode") {
		t.Fatalf("Load unknown wallet_event_tag_mode error = %v", err)
	}
}

func writeWalletConfig(t *testing.T, body string) string {
	t.Helper()
	path := filepath.Join(t.TempDir(), "config.yaml")
	if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
		t.Fatalf("write config: %v", err)
	}
	return path
}

func TestLoadTencentExampleSplitsRealtimeOutboxAndRaisesThroughput(t *testing.T) {
	cfg, err := Load("../../configs/config.tencent.example.yaml")
	if err != nil {
		t.Fatalf("Load tencent example failed: %v", err)
	}
	if !cfg.RocketMQ.Enabled || !cfg.RocketMQ.WalletOutbox.Enabled || !cfg.RocketMQ.RealtimeOutbox.Enabled {
		t.Fatalf("tencent example must enable both wallet outbox producers: %+v", cfg.RocketMQ)
	}
	if cfg.RocketMQ.WalletOutbox.Topic == cfg.RocketMQ.RealtimeOutbox.Topic {
		t.Fatalf("realtime outbox topic must be separated from generic wallet topic: %+v", cfg.RocketMQ)
	}
	if cfg.OutboxWorker.BatchSize != 200 || cfg.OutboxWorker.Concurrency != 8 {
		t.Fatalf("generic outbox throughput config mismatch: %+v", cfg.OutboxWorker)
	}
	if cfg.RealtimeOutboxWorker.BatchSize != 50 || cfg.RealtimeOutboxWorker.Concurrency != 4 {
		t.Fatalf("realtime outbox worker config mismatch: %+v", cfg.RealtimeOutboxWorker)
	}
	if len(cfg.RealtimeOutboxWorker.EventTypes) != 3 || cfg.RealtimeOutboxWorker.EventTypes[0] != "WalletRedPacketCreated" {
		t.Fatalf("realtime outbox event types mismatch: %+v", cfg.RealtimeOutboxWorker.EventTypes)
	}
}

func TestBinanceConfigDefaultsAndAccountSelection(t *testing.T) {
	cfg := normalizeBinanceConfig(BinanceConfig{
		Accounts: map[string]BinanceAccountConfig{
			" LALU ": {
				APIKey:       " lalu-key ",
				APISecretKey: " lalu-secret ",
			},
			"aslan": {
				APIKey:       "aslan-key",
				APISecretKey: "",
			},
		},
	})
	if cfg.APIBaseURL != "https://api.binance.com" || cfg.RecvWindow != 10000 || cfg.HTTPTimeout == 0 {
		t.Fatalf("binance defaults mismatch: %+v", cfg)
	}
	if !cfg.HasAnyAccount() {
		t.Fatalf("binance should detect at least one complete account")
	}
	lalu, ok := cfg.Account(" LALU ")
	if !ok || lalu.APIKey != "lalu-key" || lalu.APISecretKey != "lalu-secret" {
		t.Fatalf("lalu account selection mismatch: ok=%v account=%+v", ok, lalu)
	}
	if _, ok := cfg.Account("aslan"); ok {
		t.Fatalf("incomplete aslan account must not be selectable")
	}
	if _, ok := cfg.Account("unknown"); ok {
		t.Fatalf("unknown app must not fall back to lalu")
	}
}

func TestBinanceConfigNormalizesLegacyTopLevelAccountMap(t *testing.T) {
	var cfg Config
	// 真实反序列化旧版顶层账号，证明线上 bian.<app_code> 无需与新二进制同步切换格式。
	if err := yaml.Unmarshal([]byte(`bian:
  enabled: true
  Legacy-App:
    api_key: " key "
    api_secret_key: " secret "
`), &cfg); err != nil {
		t.Fatalf("decode legacy Binance config: %v", err)
	}
	cfg.Binance = normalizeBinanceConfig(cfg.Binance)
	account, ok := cfg.Binance.Account("legacy-app")
	if !ok || account.APIKey != "key" || account.APISecretKey != "secret" {
		t.Fatalf("legacy Binance account normalization mismatch: ok=%v account=%+v", ok, account)
	}
}

func TestExternalRechargeAddressMapNormalizesAppOverrides(t *testing.T) {
	addresses := normalizeExternalRechargeAddressMap(map[string]string{
		" ASLAN ": " TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J ",
		"":        "TEmptyApp",
		"yumi":    "",
	})
	if len(addresses) != 1 || addresses["aslan"] != "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J" {
		t.Fatalf("address map normalization mismatch: %+v", addresses)
	}
}

func TestLoadMifaPayMerchantConfig(t *testing.T) {
	cases := []struct {
		path       string
		env        string
		merNo      string
		merAccount string
		wantKey    bool
	}{
		{
			path:       "../../configs/config.yaml",
			env:        "test",
			merNo:      "10001925",
			merAccount: "d00f5fc97a114a0db291eceaf9841739",
			wantKey:    true,
		},
		{
			path:       "../../configs/config.docker.yaml",
			env:        "test",
			merNo:      "10001925",
			merAccount: "d00f5fc97a114a0db291eceaf9841739",
		},
		{
			path:       "../../configs/config.tencent.example.yaml",
			env:        "prod",
			merNo:      "10001910",
			merAccount: "6000055d34f445418124ab7c0beb81a6",
		},
	}
	for _, tc := range cases {
		cfg, err := Load(tc.path)
		if err != nil {
			t.Fatalf("Load %s failed: %v", tc.path, err)
		}
		if cfg.MifaPay.Environment != tc.env || cfg.MifaPay.MerNo != tc.merNo || cfg.MifaPay.MerAccount != tc.merAccount {
			t.Fatalf("mifapay merchant identity mismatch in %s: env=%q mer_no=%q mer_account=%q", tc.path, cfg.MifaPay.Environment, cfg.MifaPay.MerNo, cfg.MifaPay.MerAccount)
		}
		if tc.wantKey {
			assertRSAPrivateKey(t, tc.path+" private_key", cfg.MifaPay.PrivateKey)
		}
		if strings.TrimSpace(cfg.MifaPay.MerchantPublicKey) != "" {
			assertRSAPublicKey(t, tc.path+" merchant_public_key", cfg.MifaPay.MerchantPublicKey)
			// MiFaPay 只保存商户公钥来验请求签名；本地配置同时持有私钥时，提前校验密钥对避免运行后才出现 Signature error。
			if tc.wantKey {
				assertRSAPrivateKeyMatchesPublicKey(t, tc.path+" mifapay keypair", cfg.MifaPay.PrivateKey, cfg.MifaPay.MerchantPublicKey)
			}
		}
		assertRSAPublicKey(t, tc.path+" platform_public_key", cfg.MifaPay.PlatformPublicKey)
	}
}

func TestLoadMifaPayTestEndpointKeyPair(t *testing.T) {
	cfg, err := Load("../../configs/config.yaml")
	if err != nil {
		t.Fatalf("Load local config failed: %v", err)
	}
	testCfg := normalizeMifaPayConfig(MifaPayConfig{
		Environment:     "test",
		EnvironmentTest: cfg.MifaPay.EnvironmentTest,
	})
	if testCfg.Environment != "test" || testCfg.MerNo != "10001925" || testCfg.MerAccount != "d00f5fc97a114a0db291eceaf9841739" {
		t.Fatalf("mifapay test endpoint identity mismatch: %+v", testCfg)
	}
	assertRSAPrivateKey(t, "mifapay test private_key", testCfg.PrivateKey)
	assertRSAPublicKey(t, "mifapay test merchant_public_key", testCfg.MerchantPublicKey)
	// test 环境用于本地联调；请求签名只用私钥，merchant_public_key 作为后台上传值必须和私钥保持一致。
	assertRSAPrivateKeyMatchesPublicKey(t, "mifapay test keypair", testCfg.PrivateKey, testCfg.MerchantPublicKey)
}

func assertRSAPrivateKey(t *testing.T, name string, value string) {
	t.Helper()
	clean := strings.NewReplacer("\n", "", "\r", "", " ", "").Replace(strings.TrimSpace(value))
	raw, err := base64.StdEncoding.DecodeString(clean)
	if err != nil {
		t.Fatalf("%s should be base64 DER private key: %v", name, err)
	}
	key, err := x509.ParsePKCS8PrivateKey(raw)
	if err != nil {
		t.Fatalf("%s should parse as PKCS8 private key: %v", name, err)
	}
	if _, ok := key.(*rsa.PrivateKey); !ok {
		t.Fatalf("%s should be RSA private key, got %T", name, key)
	}
}

func assertRSAPublicKey(t *testing.T, name string, value string) {
	t.Helper()
	clean := strings.NewReplacer("\n", "", "\r", "", " ", "").Replace(strings.TrimSpace(value))
	raw, err := base64.StdEncoding.DecodeString(clean)
	if err != nil {
		t.Fatalf("%s should be base64 DER public key: %v", name, err)
	}
	key, err := x509.ParsePKIXPublicKey(raw)
	if err != nil {
		t.Fatalf("%s should parse as PKIX public key: %v", name, err)
	}
	if _, ok := key.(*rsa.PublicKey); !ok {
		t.Fatalf("%s should be RSA public key, got %T", name, key)
	}
}

func assertRSAPrivateKeyMatchesPublicKey(t *testing.T, name string, privateValue string, publicValue string) {
	t.Helper()
	privateClean := strings.NewReplacer("\n", "", "\r", "", " ", "").Replace(strings.TrimSpace(privateValue))
	privateRaw, err := base64.StdEncoding.DecodeString(privateClean)
	if err != nil {
		t.Fatalf("%s private key should be base64 DER: %v", name, err)
	}
	privateParsed, err := x509.ParsePKCS8PrivateKey(privateRaw)
	if err != nil {
		t.Fatalf("%s private key should parse as PKCS8: %v", name, err)
	}
	privateKey, ok := privateParsed.(*rsa.PrivateKey)
	if !ok {
		t.Fatalf("%s private key should be RSA, got %T", name, privateParsed)
	}
	publicClean := strings.NewReplacer("\n", "", "\r", "", " ", "").Replace(strings.TrimSpace(publicValue))
	publicRaw, err := base64.StdEncoding.DecodeString(publicClean)
	if err != nil {
		t.Fatalf("%s public key should be base64 DER: %v", name, err)
	}
	publicParsed, err := x509.ParsePKIXPublicKey(publicRaw)
	if err != nil {
		t.Fatalf("%s public key should parse as PKIX: %v", name, err)
	}
	publicKey, ok := publicParsed.(*rsa.PublicKey)
	if !ok {
		t.Fatalf("%s public key should be RSA, got %T", name, publicParsed)
	}
	if privateKey.N.Cmp(publicKey.N) != 0 || privateKey.E != publicKey.E {
		t.Fatalf("%s private key does not match merchant public key", name)
	}
}
