package config

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

// TestLoad 验证 gateway-service 的本地 config.yaml 能被正常读取。
func TestLoad(t *testing.T) {
	cfg, err := Load("../../configs/config.yaml")
	if err != nil {
		t.Fatalf("Load failed: %v", err)
	}

	if cfg.HTTPAddr != ":13000" {
		t.Fatalf("unexpected http addr: %s", cfg.HTTPAddr)
	}

	if cfg.RoomServiceAddr != "127.0.0.1:13001" {
		t.Fatalf("unexpected room addr: %s", cfg.RoomServiceAddr)
	}
	if !cfg.CORS.Enabled || !containsString(cfg.CORS.AllowedOrigins, "*") || !containsString(cfg.CORS.AllowedHeaders, "X-App-Code") || !containsString(cfg.CORS.AllowedHeaders, "X-App-Version") || !containsString(cfg.CORS.AllowedHeaders, "X-App-Build-Number") {
		t.Fatalf("unexpected cors config: %+v", cfg.CORS)
	}
	if cfg.GRPCClient.DefaultTimeout != 5*time.Second || cfg.GRPCClient.RetryMaxAttempts != 2 || len(cfg.GRPCClient.RetryableStatusCodes) != 1 || cfg.GRPCClient.RetryableStatusCodes[0] != "UNAVAILABLE" {
		t.Fatalf("unexpected grpc client policy: %+v", cfg.GRPCClient)
	}
	if cfg.AppConfig.MySQLDSN == "" || !strings.Contains(cfg.AppConfig.MySQLDSN, "hyapp_admin") {
		t.Fatalf("gateway app config mysql dsn must point to admin config db: %+v", cfg.AppConfig)
	}
	if cfg.Leaderboard.RedisAddr != "127.0.0.1:13379" || cfg.Leaderboard.KeyPrefix != "activity:user_leaderboard" {
		t.Fatalf("local leaderboard redis config mismatch: %+v", cfg.Leaderboard)
	}
	if !cfg.AuthRateLimit.Enabled || cfg.AuthRateLimit.RedisAddr != "127.0.0.1:13379" {
		t.Fatalf("local auth rate redis config mismatch: %+v", cfg.AuthRateLimit)
	}
	if cfg.AuthRateLimit.KeyPrefix != "gateway:auth:rate:" {
		t.Fatalf("unexpected auth rate key prefix: %q", cfg.AuthRateLimit.KeyPrefix)
	}
	if !cfg.GiftCapacityLimit.Enabled || cfg.GiftCapacityLimit.RedisAddr != "127.0.0.1:13379" || cfg.GiftCapacityLimit.KeyPrefix != "gateway:gift:capacity:" {
		t.Fatalf("local gift capacity config mismatch: %+v", cfg.GiftCapacityLimit)
	}
	if cfg.GiftCapacityLimit.UserBucketCapacity != 5_000 || cfg.GiftCapacityLimit.UserRefillTokensPerSecond != 500 || cfg.GiftCapacityLimit.RoomBucketCapacity != 20_000 || cfg.GiftCapacityLimit.RoomRefillTokensPerSecond != 5_000 || cfg.GiftCapacityLimit.AppBucketCapacity != 100_000 || cfg.GiftCapacityLimit.AppRefillTokensPerSecond != 25_000 {
		t.Fatalf("unexpected local gift capacity buckets: %+v", cfg.GiftCapacityLimit)
	}
	if cfg.GiftCapacityLimit.MaxTargetsPerRequest != 30 || cfg.GiftCapacityLimit.MaxGiftCountPerRequest != 999 || cfg.GiftCapacityLimit.MaxGiftUnitsPerRequest != 5_000 || cfg.GiftCapacityLimit.MaxInFlight != 2 || cfg.GiftCapacityLimit.InFlightLease != 10*time.Second || cfg.GiftCapacityLimit.RedisOperationTimeout != 500*time.Millisecond || cfg.GiftCapacityLimit.RequestTimeout != 5*time.Second {
		t.Fatalf("unexpected local gift capacity policy: %+v", cfg.GiftCapacityLimit)
	}
	if !cfg.TencentRTC.Enabled || cfg.TencentRTC.SDKAppID == 0 || cfg.TencentRTC.SecretKey == "" {
		t.Fatalf("local tencent rtc config must be explicit: %+v", cfg.TencentRTC)
	}
	if cfg.TencentRTC.UserSigTTL != 2*time.Hour || cfg.TencentRTC.RoomIDType != "string" || cfg.TencentRTC.AppScene != "voice_chat_room" {
		t.Fatalf("unexpected local tencent rtc policy: %+v", cfg.TencentRTC)
	}
	if !cfg.TencentCOS.Enabled || cfg.TencentCOS.SecretID == "" || cfg.TencentCOS.SecretKey == "" || cfg.TencentCOS.BucketName == "" || cfg.TencentCOS.Region == "" || cfg.TencentCOS.AccessURL == "" {
		t.Fatalf("local tencent cos config must be explicit")
	}
	if cfg.TencentCOS.AccessURL != "https://media.haiyihy.com" {
		t.Fatalf("unexpected local tencent cos access url: %q", cfg.TencentCOS.AccessURL)
	}
	if !cfg.FinanceNotifications.DingTalk.Enabled {
		t.Fatalf("finance dingtalk notification should default to enabled")
	}
}

// TestLoadTencentExample 验证腾讯云 IM 线上模板字段能被 gateway 正确解析。
func TestLoadTencentExample(t *testing.T) {
	cfg, err := Load("../../configs/config.tencent.example.yaml")
	if err != nil {
		t.Fatalf("Load tencent example failed: %v", err)
	}

	if cfg.TencentIM.SDKAppID == 0 {
		t.Fatalf("sdk_app_id template must be present")
	}
	if cfg.TencentIM.SecretKey == "" || cfg.TencentIM.CallbackAuthToken == "" {
		t.Fatalf("secret and callback token templates must be present")
	}
	if cfg.TencentIM.CallbackURL == "" {
		t.Fatalf("callback_url template must be present")
	}
	if !cfg.CORS.Enabled || !containsString(cfg.CORS.AllowedOrigins, "https://h5.global-interaction.com") || !containsString(cfg.CORS.AllowedHeaders, "X-App-Version") || !containsString(cfg.CORS.AllowedHeaders, "X-App-Build-Number") || cfg.CORS.MaxAgeSec != 600 {
		t.Fatalf("tencent cors template must be explicit: %+v", cfg.CORS)
	}
	if cfg.AppConfig.MySQLDSN == "" || !strings.Contains(cfg.AppConfig.MySQLDSN, "hyapp_admin") {
		t.Fatalf("tencent app config mysql dsn template must be explicit: %+v", cfg.AppConfig)
	}
	if cfg.Leaderboard.RedisAddr == "" || cfg.Leaderboard.RedisPassword == "" || cfg.Leaderboard.KeyPrefix != "activity:user_leaderboard" {
		t.Fatalf("tencent leaderboard redis template must be explicit: %+v", cfg.Leaderboard)
	}
	if cfg.TencentIM.UserSigTTL != 24*time.Hour {
		t.Fatalf("unexpected usersig ttl: %s", cfg.TencentIM.UserSigTTL)
	}
	if !cfg.AuthRateLimit.Enabled || cfg.AuthRateLimit.RedisAddr == "" || cfg.AuthRateLimit.RedisPassword == "" {
		t.Fatalf("tencent auth rate redis template must be explicit: %+v", cfg.AuthRateLimit)
	}
	if !cfg.GiftCapacityLimit.Enabled || cfg.GiftCapacityLimit.RedisAddr == "" || cfg.GiftCapacityLimit.RedisPassword == "" || cfg.GiftCapacityLimit.InFlightLease != 10*time.Second || cfg.GiftCapacityLimit.RequestTimeout != 5*time.Second || cfg.GiftCapacityLimit.MaxTargetsPerRequest != 30 {
		t.Fatalf("tencent gift capacity template must be explicit: %+v", cfg.GiftCapacityLimit)
	}
	if !cfg.TencentRTC.Enabled || cfg.TencentRTC.SDKAppID == 0 || cfg.TencentRTC.SecretKey == "" {
		t.Fatalf("tencent rtc template must be explicit: %+v", cfg.TencentRTC)
	}
	if cfg.TencentRTC.UserSigTTL != 2*time.Hour || cfg.TencentRTC.RoomIDType != "string" || cfg.TencentRTC.AppScene != "voice_chat_room" {
		t.Fatalf("unexpected tencent rtc policy: %+v", cfg.TencentRTC)
	}
	if !cfg.TencentCOS.Enabled || cfg.TencentCOS.SecretID == "" || cfg.TencentCOS.SecretKey == "" || cfg.TencentCOS.BucketName == "" || cfg.TencentCOS.Region == "" || cfg.TencentCOS.AccessURL == "" {
		t.Fatalf("tencent cos template must be explicit")
	}
	if !cfg.FinanceNotifications.DingTalk.Enabled || cfg.FinanceNotifications.DingTalk.WebhookURL == "" {
		t.Fatalf("tencent finance dingtalk template must be enabled with an absolute webhook placeholder: %+v", cfg.FinanceNotifications.DingTalk)
	}
}

func TestLoadDockerGiftCapacityLimit(t *testing.T) {
	cfg, err := Load("../../configs/config.docker.yaml")
	if err != nil {
		t.Fatalf("Load Docker config failed: %v", err)
	}
	if !cfg.GiftCapacityLimit.Enabled || cfg.GiftCapacityLimit.RedisAddr != "redis:6379" || cfg.GiftCapacityLimit.UserBucketCapacity != 5_000 || cfg.GiftCapacityLimit.RoomBucketCapacity != 20_000 || cfg.GiftCapacityLimit.AppBucketCapacity != 100_000 || cfg.GiftCapacityLimit.MaxInFlight != 2 {
		t.Fatalf("Docker gift capacity config mismatch: %+v", cfg.GiftCapacityLimit)
	}
	if !containsString(cfg.CORS.AllowedHeaders, "X-App-Version") || !containsString(cfg.CORS.AllowedHeaders, "X-App-Build-Number") {
		t.Fatalf("Docker CORS must allow login client version headers: %+v", cfg.CORS)
	}
}

func TestLoadLegacyYAMLWithoutGiftSectionReusesAuthRedis(t *testing.T) {
	if got := Default().GiftCapacityLimit.RedisAddr; got != "" {
		t.Fatalf("Default gift RedisAddr must stay empty for auth Redis inheritance, got %q", got)
	}
	path := filepath.Join(t.TempDir(), "config.yaml")
	if err := os.WriteFile(path, []byte(`auth_rate_limit:
  enabled: true
  redis_addr: "legacy-redis.internal:6379"
  redis_password: "legacy-secret"
  redis_db: 4
`), 0o600); err != nil {
		t.Fatalf("write legacy config failed: %v", err)
	}
	cfg, err := Load(path)
	if err != nil {
		t.Fatalf("legacy YAML without gift_capacity_limit must still load: %v", err)
	}
	if !cfg.GiftCapacityLimit.Enabled || cfg.GiftCapacityLimit.RedisAddr != "legacy-redis.internal:6379" || cfg.GiftCapacityLimit.RedisPassword != "legacy-secret" || cfg.GiftCapacityLimit.RedisDB != 4 {
		t.Fatalf("gift capacity must reuse auth Redis for old YAML: %+v", cfg.GiftCapacityLimit)
	}
	if cfg.GiftCapacityLimit.MaxTargetsPerRequest != 30 || cfg.GiftCapacityLimit.MaxGiftCountPerRequest != 999 || cfg.GiftCapacityLimit.MaxGiftUnitsPerRequest != 5_000 || cfg.GiftCapacityLimit.UserRefillTokensPerSecond != 500 || cfg.GiftCapacityLimit.RoomRefillTokensPerSecond != 5_000 || cfg.GiftCapacityLimit.AppRefillTokensPerSecond != 25_000 {
		t.Fatalf("old YAML must receive safe capacity defaults: %+v", cfg.GiftCapacityLimit)
	}
}

func TestNormalizeLoginRiskDisabledStillInheritsMandatorySessionDenylistRedis(t *testing.T) {
	cfg := Default()
	cfg.AuthRateLimit.RedisAddr = "shared-redis.internal:6379"
	cfg.AuthRateLimit.RedisPassword = "shared-secret"
	cfg.AuthRateLimit.RedisDB = 4
	cfg.LoginRisk.Enabled = false
	cfg.LoginRisk.RedisAddr = ""
	cfg.LoginRisk.RedisPassword = ""
	cfg.LoginRisk.RedisDB = 0
	if err := cfg.Normalize(); err != nil {
		t.Fatalf("normalize login-risk disabled config failed: %v", err)
	}
	if cfg.LoginRisk.RedisAddr != "shared-redis.internal:6379" || cfg.LoginRisk.RedisPassword != "shared-secret" || cfg.LoginRisk.RedisDB != 4 {
		t.Fatalf("disabled FastGuard must still inherit mandatory session denylist Redis: %+v", cfg.LoginRisk)
	}
}

func TestNormalizeRejectsGiftLeaseWithoutRequestSafetyMargin(t *testing.T) {
	cfg := Default()
	cfg.GiftCapacityLimit.RequestTimeout = 8 * time.Second
	cfg.GiftCapacityLimit.InFlightLease = 10 * time.Second
	if err := cfg.Normalize(); err == nil || !strings.Contains(err.Error(), "in_flight_lease") {
		t.Fatalf("lease equal to request timeout plus safety margin must be rejected: %v", err)
	}
}

func TestNormalizeRejectsGiftBucketSmallerThanRequestMaximum(t *testing.T) {
	cfg := Default()
	cfg.GiftCapacityLimit.MaxGiftUnitsPerRequest = 5_000
	cfg.GiftCapacityLimit.UserBucketCapacity = 4_999
	if err := cfg.Normalize(); err == nil || !strings.Contains(err.Error(), "bucket capacities") {
		t.Fatalf("bucket below max request weight must be rejected: %v", err)
	}
}

func containsString(values []string, want string) bool {
	for _, value := range values {
		if value == want {
			return true
		}
	}
	return false
}

// TestLoadRejectsUnsupportedRTCRoomIDType protects the string strRoomId policy at startup.
func TestLoadRejectsUnsupportedRTCRoomIDType(t *testing.T) {
	path := filepath.Join(t.TempDir(), "config.yaml")
	if err := os.WriteFile(path, []byte(`tencent_rtc:
  enabled: true
  sdk_app_id: 1400000000
  secret_key: "secret"
  user_sig_ttl: "2h"
  room_id_type: "number"
  app_scene: "voice_chat_room"
`), 0o600); err != nil {
		t.Fatalf("write temp config failed: %v", err)
	}

	if _, err := Load(path); err == nil {
		t.Fatalf("Load should reject unsupported tencent_rtc.room_id_type")
	}
}

func TestLoadRejectsIncompleteTencentCOSConfigWhenEnabled(t *testing.T) {
	path := filepath.Join(t.TempDir(), "config.yaml")
	if err := os.WriteFile(path, []byte(`tencent-cos:
  enabled: true
  secret-id: "secret-id"
  secret-key: ""
  bucket-name: "bucket"
  region: "me-saudi-arabia"
  access-url: "https://media.example.com"
`), 0o600); err != nil {
		t.Fatalf("write temp config failed: %v", err)
	}

	if _, err := Load(path); err == nil {
		t.Fatalf("Load should reject incomplete enabled tencent-cos config")
	}
}
