// Package config 验证 room-service 本地 YAML 配置契约。
package config

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

	"hyapp/pkg/roommq"
)

// TestLoad 验证 room-service 的本地 config.yaml 能被正常读取。
func TestLoad(t *testing.T) {
	// 读取真实本地配置，确保端口和关键下游地址没有漂移。
	cfg, err := Load("../../configs/config.yaml")
	if err != nil {
		t.Fatalf("Load failed: %v", err)
	}

	// room-service 业务只走 gRPC，HTTP health 仅服务 CLB 探活，端口必须保持 13xxx 约束。
	if cfg.GRPCAddr != ":13001" {
		t.Fatalf("unexpected grpc addr: %s", cfg.GRPCAddr)
	}
	if cfg.HealthHTTPAddr != ":13101" {
		t.Fatalf("unexpected health http addr: %s", cfg.HealthHTTPAddr)
	}
	if cfg.AdvertiseAddr != "127.0.0.1:13001" || cfg.NodeRegistryTTL != 30*time.Second || cfg.NodeRegistryHeartbeatInterval != 10*time.Second || cfg.OwnerForwardTimeout != 2*time.Second {
		t.Fatalf("unexpected owner routing config: advertise=%s ttl=%s heartbeat=%s timeout=%s", cfg.AdvertiseAddr, cfg.NodeRegistryTTL, cfg.NodeRegistryHeartbeatInterval, cfg.OwnerForwardTimeout)
	}

	// lease TTL 是故障接管抖动窗口的基础参数。
	if cfg.LeaseTTL != 10*time.Second {
		t.Fatalf("unexpected lease ttl: %s", cfg.LeaseTTL)
	}
	if cfg.RoomListCacheRefreshInterval != 5*time.Minute {
		t.Fatalf("unexpected room list cache refresh interval: %s", cfg.RoomListCacheRefreshInterval)
	}

	// SendGift 主链路依赖 wallet-service，默认地址必须与本地编排一致。
	if cfg.WalletServiceAddr != "127.0.0.1:13004" {
		t.Fatalf("unexpected wallet addr: %s", cfg.WalletServiceAddr)
	}
	if cfg.TencentIM.SDKAppID != 20040101 || cfg.TencentIM.SecretKey == "" || cfg.TencentIM.AdminIdentifier != "administrator" {
		t.Fatalf("local room-service must use test Tencent IM account: %+v", cfg.TencentIM)
	}
	if !cfg.OutboxWorker.Enabled || cfg.OutboxWorker.PublishMode != OutboxPublishModeMQ || cfg.OutboxWorker.PollInterval != 200*time.Millisecond || cfg.OutboxWorker.BatchSize != 50 || cfg.OutboxWorker.Concurrency != 16 || cfg.OutboxWorker.PublishTimeout != 15*time.Second || cfg.OutboxWorker.RetryStrategy != "exponential_backoff" || cfg.OutboxWorker.MaxRetryCount != 10 || cfg.OutboxWorker.InitialBackoff != 5*time.Second || cfg.OutboxWorker.MaxBackoff != 5*time.Minute {
		t.Fatalf("unexpected outbox worker config: %+v", cfg.OutboxWorker)
	}
	// 本地默认仍走 room_outbox MQ 和火箭延迟唤醒；房间 UI IM 已改为 Room Cell 提交后直发，不再启 MQ IM bridge。
	if !cfg.RocketMQ.Enabled || !cfg.RocketMQ.RoomOutbox.Enabled || cfg.RocketMQ.RoomOutbox.TencentIMConsumerEnabled || !cfg.RocketMQ.RocketLaunch.Enabled || !cfg.RocketMQ.UserOutbox.Enabled || !cfg.RocketMQ.WalletRealtimeOutbox.Enabled || !cfg.RocketMQ.LuckyGiftOutbox.Enabled {
		t.Fatalf("local config must enable room MQ fanout and disable MQ IM bridge: %+v", cfg.RocketMQ)
	}
	if cfg.RocketMQ.LuckyGiftOutbox.Topic != "hyapp_lucky_gift_outbox" || cfg.RocketMQ.LuckyGiftOutbox.ConsumerGroup != "hyapp-room-lucky-gift-outbox" {
		t.Fatalf("local config must isolate lucky gift display consumption: %+v", cfg.RocketMQ.LuckyGiftOutbox)
	}
	if cfg.RocketMQ.WalletRealtimeOutbox.Topic != "hyapp_wallet_realtime_outbox" || cfg.RocketMQ.WalletRealtimeOutbox.ConsumerGroup != "hyapp-room-wallet-red-packet-projection" {
		t.Fatalf("local config must isolate red packet room-list projection: %+v", cfg.RocketMQ.WalletRealtimeOutbox)
	}
	if cfg.RocketMQ.RoomEventTagMode != roommq.EventTagModeLegacy {
		t.Fatalf("Phase A config must publish legacy room tags: %q", cfg.RocketMQ.RoomEventTagMode)
	}
}

func TestRoomEventTagModeDefaultsAndValidation(t *testing.T) {
	if got := Default().RocketMQ.RoomEventTagMode; got != roommq.EventTagModeLegacy {
		t.Fatalf("default room event tag mode = %q, want %q", got, roommq.EventTagModeLegacy)
	}

	missing, err := Load(writeRoomConfig(t, "rocketmq:\n  enabled: false\n"))
	if err != nil {
		t.Fatalf("Load missing room tag mode failed: %v", err)
	}
	if missing.RocketMQ.RoomEventTagMode != roommq.EventTagModeLegacy {
		t.Fatalf("missing room_event_tag_mode = %q", missing.RocketMQ.RoomEventTagMode)
	}

	typed, err := Load(writeRoomConfig(t, "rocketmq:\n  enabled: false\n  room_event_tag_mode: ' EVENT_TYPE '\n"))
	if err != nil {
		t.Fatalf("Load Phase B room tag mode failed: %v", err)
	}
	if typed.RocketMQ.RoomEventTagMode != roommq.EventTagModeEventType {
		t.Fatalf("room_event_tag_mode = %q, want %q", typed.RocketMQ.RoomEventTagMode, roommq.EventTagModeEventType)
	}

	if _, err := Load(writeRoomConfig(t, "rocketmq:\n  enabled: false\n  room_event_tag_mode: wildcard\n")); err == nil || !strings.Contains(err.Error(), "room_event_tag_mode") {
		t.Fatalf("Load unknown room_event_tag_mode error = %v", err)
	}
}

func writeRoomConfig(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 room config: %v", err)
	}
	return path
}

// TestLoadTencentExample 验证线上腾讯云 IM 模板能覆盖本地默认配置。
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.Enabled {
		t.Fatalf("tencent example should enable Tencent IM direct publisher")
	}
	if cfg.TencentIM.SDKAppID == 0 || cfg.TencentIM.SecretKey == "" {
		t.Fatalf("sdk_app_id and secret_key templates must be present")
	}
	if cfg.TencentIM.AdminIdentifier == "" {
		t.Fatalf("admin_identifier template must be present")
	}
	if cfg.TencentIM.Endpoint == "" || cfg.TencentIM.GroupType == "" {
		t.Fatalf("endpoint and group_type templates must be present")
	}
	if cfg.TencentIM.RequestTimeout != 5*time.Second {
		t.Fatalf("unexpected request timeout: %s", cfg.TencentIM.RequestTimeout)
	}
	if cfg.RoomListCacheRefreshInterval != 5*time.Minute {
		t.Fatalf("unexpected room list cache refresh interval: %s", cfg.RoomListCacheRefreshInterval)
	}
	if cfg.AdvertiseAddr == "" || cfg.AdvertiseAddr == "room-service.internal:13001" {
		t.Fatalf("tencent example must use per-instance advertise_addr, got %q", cfg.AdvertiseAddr)
	}
	if !cfg.OutboxWorker.Enabled || cfg.OutboxWorker.PollInterval != 200*time.Millisecond || cfg.OutboxWorker.BatchSize != 50 || cfg.OutboxWorker.Concurrency != 16 || cfg.OutboxWorker.PublishTimeout != 15*time.Second || cfg.OutboxWorker.RetryStrategy != "exponential_backoff" || cfg.OutboxWorker.MaxRetryCount != 10 {
		t.Fatalf("tencent example must configure outbox worker: %+v", cfg.OutboxWorker)
	}
	if cfg.OutboxWorker.PublishMode != OutboxPublishModeMQ || !cfg.RocketMQ.RoomOutbox.Enabled || !cfg.RocketMQ.RocketLaunch.Enabled || !cfg.RocketMQ.WalletRealtimeOutbox.Enabled || !cfg.RocketMQ.LuckyGiftOutbox.Enabled {
		t.Fatalf("tencent example must route room outbox and rocket launch through RocketMQ: mode=%s mq=%+v", cfg.OutboxWorker.PublishMode, cfg.RocketMQ)
	}
	if cfg.RocketMQ.RoomEventTagMode != roommq.EventTagModeLegacy {
		t.Fatalf("tencent example must remain in Phase A until consumers are verified: %q", cfg.RocketMQ.RoomEventTagMode)
	}
}

func TestLoadDockerConfigDoesNotStartRoomIMBridgeWhenOutboxUsesMQ(t *testing.T) {
	cfg, err := Load("../../configs/config.docker.yaml")
	if err != nil {
		t.Fatalf("Load docker config failed: %v", err)
	}

	if cfg.OutboxWorker.PublishMode != OutboxPublishModeMQ {
		t.Fatalf("docker config should publish room outbox through MQ, got %q", cfg.OutboxWorker.PublishMode)
	}
	if cfg.OutboxWorker.PollInterval != 200*time.Millisecond || cfg.OutboxWorker.BatchSize != 50 || cfg.OutboxWorker.Concurrency != 16 || cfg.OutboxWorker.PublishTimeout != 15*time.Second {
		t.Fatalf("docker config must run room outbox with 16 workers, short polling, and a bounded claim window: %+v", cfg.OutboxWorker)
	}
	if !cfg.RocketMQ.RoomOutbox.Enabled {
		t.Fatalf("docker config must enable room outbox MQ")
	}
	if !cfg.RocketMQ.LuckyGiftOutbox.Enabled || cfg.RocketMQ.LuckyGiftOutbox.ConsumerGroup != "hyapp-room-lucky-gift-outbox" {
		t.Fatalf("docker config must enable an isolated lucky gift consumer: %+v", cfg.RocketMQ.LuckyGiftOutbox)
	}
	if !cfg.RocketMQ.WalletRealtimeOutbox.Enabled || cfg.RocketMQ.WalletRealtimeOutbox.ConsumerGroup != "hyapp-room-wallet-red-packet-projection" {
		t.Fatalf("docker config must enable isolated red packet projection: %+v", cfg.RocketMQ.WalletRealtimeOutbox)
	}
	if cfg.RocketMQ.RoomEventTagMode != roommq.EventTagModeLegacy {
		t.Fatalf("docker config must remain in Phase A: %q", cfg.RocketMQ.RoomEventTagMode)
	}
	if cfg.RocketMQ.RoomOutbox.TencentIMConsumerEnabled {
		t.Fatalf("docker mq room outbox must not start Tencent IM bridge consumer")
	}
	if cfg.TencentIM.SDKAppID != 20040101 || cfg.TencentIM.AdminIdentifier != "administrator" {
		t.Fatalf("docker config must use test IM account: im=%+v", cfg.TencentIM)
	}
	if !cfg.TencentIM.Enabled {
		t.Fatalf("direct room IM publishing requires tencent_im.enabled")
	}
}

func TestNormalizeRejectsUnknownOutboxRetryStrategy(t *testing.T) {
	cfg := Default()
	cfg.OutboxWorker.RetryStrategy = "exponential"

	if _, err := Normalize(cfg); err == nil {
		t.Fatal("Normalize should reject unsupported outbox retry_strategy")
	}
}

func TestNormalizeRejectsUnknownOutboxPublishMode(t *testing.T) {
	cfg := Default()
	cfg.OutboxWorker.PublishMode = "random"

	if _, err := Normalize(cfg); err == nil {
		t.Fatal("Normalize should reject unsupported outbox publish_mode")
	}
}

func TestNormalizeRejectsMQPublishModeWithoutRoomOutboxMQ(t *testing.T) {
	cfg := Default()
	cfg.OutboxWorker.PublishMode = OutboxPublishModeMQ

	if _, err := Normalize(cfg); err == nil {
		t.Fatal("Normalize should reject mq publish_mode without rocketmq.room_outbox.enabled")
	}
}

func TestNormalizeRejectsLuckyGiftConsumerWithoutTencentIM(t *testing.T) {
	cfg := Default()
	cfg.RocketMQ.LuckyGiftOutbox.Enabled = true
	cfg.RocketMQ.NameServers = []string{"127.0.0.1:9876"}

	if _, err := Normalize(cfg); err == nil || !strings.Contains(err.Error(), "tencent_im.enabled") {
		t.Fatalf("Normalize should reject lucky gift consumption without room IM publisher: %v", err)
	}
}

func TestNormalizeOutboxWorkerKeepsSafeDefaults(t *testing.T) {
	cfg := Default()
	cfg.OutboxWorker.PollInterval = 0
	cfg.OutboxWorker.BatchSize = 0
	cfg.OutboxWorker.Concurrency = 0
	cfg.OutboxWorker.PublishTimeout = 0
	cfg.OutboxWorker.RetryStrategy = ""
	cfg.OutboxWorker.MaxRetryCount = 0
	cfg.OutboxWorker.InitialBackoff = 0
	cfg.OutboxWorker.MaxBackoff = 0

	normalized, err := Normalize(cfg)
	if err != nil {
		t.Fatalf("Normalize failed: %v", err)
	}
	if normalized.OutboxWorker.PublishMode != OutboxPublishModeDirect || normalized.OutboxWorker.PollInterval != time.Second || normalized.OutboxWorker.BatchSize != 100 || normalized.OutboxWorker.Concurrency != 1 || normalized.OutboxWorker.PublishTimeout != 3*time.Second || normalized.OutboxWorker.RetryStrategy != "exponential_backoff" || normalized.OutboxWorker.MaxRetryCount != 10 || normalized.OutboxWorker.InitialBackoff != 5*time.Second || normalized.OutboxWorker.MaxBackoff != 5*time.Minute {
		t.Fatalf("outbox worker defaults not restored: %+v", normalized.OutboxWorker)
	}
}

func TestNormalizeOutboxRetentionKeepsSafeDefaults(t *testing.T) {
	cfg := Default()
	if cfg.OutboxWorker.Retention.Enabled {
		// 清理会删除统计重放依赖的历史事件，必须显式开启，默认配置绝不能自动生效。
		t.Fatal("outbox retention must default to disabled")
	}
	cfg.OutboxWorker.Retention = OutboxRetentionConfig{Enabled: true}

	normalized, err := Normalize(cfg)
	if err != nil {
		t.Fatalf("Normalize failed: %v", err)
	}
	retention := normalized.OutboxWorker.Retention
	if !retention.Enabled || retention.MaxAge != 30*24*time.Hour || retention.ScanInterval != 5*time.Minute || retention.BatchSize != 500 {
		t.Fatalf("outbox retention defaults not restored: %+v", retention)
	}
}

func TestNormalizeRejectsOutboxRetentionMaxAgeBelowSafetyFloor(t *testing.T) {
	cfg := Default()
	cfg.OutboxWorker.Retention.Enabled = true
	cfg.OutboxWorker.Retention.MaxAge = time.Hour

	if _, err := Normalize(cfg); err == nil {
		t.Fatal("Normalize should reject outbox retention max_age below 24h")
	}
}
