package luckygift

import (
	"strings"
	"testing"

	domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
)

func TestDefaultRuleConfigUsesSafeDynamicV3Draft(t *testing.T) {
	config := DefaultRuleConfig("lalu", "lucky")
	if config.StrategyVersion != domain.StrategyDynamicV3 || config.Enabled {
		t.Fatalf("default strategy=%q enabled=%v, want disabled dynamic_v3 draft", config.StrategyVersion, config.Enabled)
	}
	if config.TargetRTPPPM != 980_000 || config.PoolRatePPM != 980_000 || config.ProfitRatePPM != 10_000 || config.AnchorRatePPM != 10_000 {
		t.Fatalf("default RTP/fund split mismatch: %+v", config)
	}
	if config.ControlBandPPM != 30_000 || config.InitialPoolCoins != 0 || config.LossStreakGuarantee != 5 {
		t.Fatalf("default control values mismatch: %+v", config)
	}
	if config.LowWatermarkCoins != 10_000_000 || config.LowWaterNonzeroFactorPPM != 700_000 ||
		config.HighWatermarkCoins != 20_000_000 || config.HighWaterNonzeroFactorPPM != 1_300_000 {
		t.Fatalf("default water controls mismatch: %+v", config)
	}
	if config.RechargeBoostWindowMS != 300_000 || config.RechargeBoostFactorPPM != 1_100_000 {
		t.Fatalf("default recharge boost mismatch: %+v", config)
	}
	if config.JackpotGlobalRTPMaxPPM != 980_000 || config.JackpotUserDayRTPMaxPPM != 960_000 ||
		config.JackpotUser72hRTPMaxPPM != 960_000 || config.MaxJackpotHitsPerUserDay != 5 {
		t.Fatalf("default jackpot controls mismatch: %+v", config)
	}
	if config.JackpotSpendThresholdCoins != 0 || config.MaxSinglePayout != 0 || config.AnchorDailyPayoutCap != 0 {
		t.Fatalf("App-specific monetary limits must stay unset in disabled draft: %+v", config)
	}

	wantThresholds := [][2]int64{{0, 0}, {0, 0}, {0, 0}}
	for index, stage := range config.Stages {
		if got := [2]int64{stage.MinRecharge7DCoins, stage.MinRecharge30DCoins}; got != wantThresholds[index] {
			t.Fatalf("stage %s thresholds=%v, want %v", stage.Stage, got, wantThresholds[index])
		}
		var expectedRTPPPM int64
		for _, tier := range stage.Tiers {
			if tier.Enabled {
				expectedRTPPPM += tier.MultiplierPPM * tier.BaseWeightPPM / ppmScale
			}
		}
		if expectedRTPPPM != 980_000 {
			t.Fatalf("stage %s static expected RTP=%d, want safe 980000", stage.Stage, expectedRTPPPM)
		}
	}
	if err := validateRuleConfig(config); err != nil {
		t.Fatalf("disabled dynamic_v3 draft should validate before App-specific monetary limits are known: %v", err)
	}
	config.Enabled = true
	if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), "normal stage") {
		t.Fatalf("enabled draft error=%v, want explicit normal recharge threshold guidance", err)
	}
}

func TestValidateDynamicV3RejectsImplicitInitialFunding(t *testing.T) {
	config := DefaultRuleConfig("lalu", "lucky")
	config.InitialPoolCoins = 1_000_000
	if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), "audited credit adjustment") {
		t.Fatalf("implicit V3 seed error=%v, want audited credit guidance", err)
	}
}

func TestValidateDynamicV3RequiresExplicitMonetaryRiskLimitsWhenEnabled(t *testing.T) {
	config := DefaultRuleConfig("lalu", "lucky")
	config.Stages[1].MinRecharge7DCoins = 100
	config.Stages[1].MinRecharge30DCoins = 500
	config.Stages[2].MinRecharge7DCoins = 500
	config.Stages[2].MinRecharge30DCoins = 2_000
	config.Enabled = true
	if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), "cumulative spend threshold") {
		t.Fatalf("enabled draft error=%v, want explicit cumulative spend coin threshold guidance", err)
	}

	config.JackpotSpendThresholdCoins = 5_000
	config.MaxSinglePayout = 10_000
	config.UserHourlyPayoutCap = 20_000
	config.UserDailyPayoutCap = 30_000
	config.DeviceDailyPayoutCap = 40_000
	config.RoomHourlyPayoutCap = 50_000
	config.AnchorDailyPayoutCap = 60_000
	if err := validateRuleConfig(config); err != nil {
		t.Fatalf("fully configured dynamic_v3 should validate: %v", err)
	}

	config.RoomHourlyPayoutCap = 0
	if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), "room_hourly_payout_cap") {
		t.Fatalf("zero six-dimensional cap error=%v, want the missing cap name", err)
	}
}

func TestValidateDynamicV3RechargeStagesAreStrictlyMonotonic(t *testing.T) {
	config := DefaultRuleConfig("lalu", "lucky")
	config.Stages[1].MinRecharge7DCoins = 100
	config.Stages[1].MinRecharge30DCoins = 500
	config.Stages[2].MinRecharge7DCoins = 500
	config.Stages[2].MinRecharge30DCoins = 2_000
	config.Stages[2].MinRecharge7DCoins = config.Stages[1].MinRecharge7DCoins
	config.Stages[2].MinRecharge30DCoins = config.Stages[1].MinRecharge30DCoins
	if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), "exceed normal") {
		t.Fatalf("equal normal/advanced thresholds error=%v, want strict stage separation", err)
	}
}

func TestValidateDynamicV3AllowsOrdinaryProbabilityAtJackpotMultiplier(t *testing.T) {
	config := DefaultRuleConfig("lalu", "lucky")
	for stageIndex := range config.Stages {
		stage := &config.Stages[stageIndex]
		for tierIndex := range stage.Tiers {
			if stage.Tiers[tierIndex].MultiplierPPM == 1_000_000 {
				stage.Tiers[tierIndex].BaseWeightPPM -= 100
			}
		}
		// 200x 同时位于 jackpot_multipliers 与普通概率表是合法配置；这里用极小权重
		// 保持静态 RTP 位于控制带内，确保测试只验证“来源可重叠”这一条边界。
		stage.Tiers = append(stage.Tiers, domain.RuleTier{
			Stage: stage.Stage, TierID: stage.Stage + "_ordinary_200x",
			MultiplierPPM: 200_000_000, BaseWeightPPM: 100, HighWaterOnly: true, Enabled: true,
		})
	}
	if err := validateRuleConfig(config); err != nil {
		t.Fatalf("ordinary and jackpot sources at the same multiplier should validate independently: %v", err)
	}
}

func TestValidateRuleConfigKeepsLegacyFixedV2Compatible(t *testing.T) {
	config := DefaultRuleConfig("lalu", "lucky")
	config.StrategyVersion = ""
	config.Enabled = true
	// 升级前请求没有任何 dynamic_v3 字段；空 strategy_version 必须按 fixed_v2 解释，不能要求动态消费门槛或六维 cap。
	config.ProfitRatePPM = 0
	config.AnchorRatePPM = 0
	config.InitialPoolCoins = 0
	config.LossStreakGuarantee = 0
	config.LowWatermarkCoins = 0
	config.LowWaterNonzeroFactorPPM = 0
	config.HighWatermarkCoins = 0
	config.HighWaterNonzeroFactorPPM = 0
	config.RechargeBoostWindowMS = 0
	config.RechargeBoostFactorPPM = 0
	config.JackpotMultiplierPPMs = nil
	config.JackpotGlobalRTPMaxPPM = 0
	config.JackpotUserDayRTPMaxPPM = 0
	config.JackpotUser72hRTPMaxPPM = 0
	config.MaxJackpotHitsPerUserDay = 0
	if err := validateRuleConfig(config); err != nil {
		t.Fatalf("legacy fixed_v2 config was broken by dynamic validation: %v", err)
	}
	normalized := normalizeRuleConfig(config)
	if normalized.StrategyVersion != domain.StrategyFixedV2 {
		t.Fatalf("legacy empty strategy normalized to %q, want fixed_v2", normalized.StrategyVersion)
	}
}

func legacyFixedV2Config() domain.RuleConfig {
	config := DefaultRuleConfig("lalu", "lucky")
	config.StrategyVersion = domain.StrategyFixedV2
	config.Enabled = true
	config.ProfitRatePPM = 0
	config.AnchorRatePPM = 0
	config.InitialPoolCoins = 0
	config.LossStreakGuarantee = 0
	config.LowWatermarkCoins = 0
	config.LowWaterNonzeroFactorPPM = 0
	config.HighWatermarkCoins = 0
	config.HighWaterNonzeroFactorPPM = 0
	config.RechargeBoostWindowMS = 0
	config.RechargeBoostFactorPPM = 0
	config.JackpotMultiplierPPMs = nil
	config.JackpotGlobalRTPMaxPPM = 0
	config.JackpotUserDayRTPMaxPPM = 0
	config.JackpotUser72hRTPMaxPPM = 0
	config.JackpotUserRoundRTPMaxPPM = 0
	config.JackpotUser48hRTPMaxPPM = 0
	config.MaxJackpotHitsPerUserDay = 0
	return config
}

func TestValidateV2HighWaterBoostAcceptsHysteresisTriple(t *testing.T) {
	config := legacyFixedV2Config()
	config.V2HighWaterBoostThresholdCoins = 2_000_000
	config.V2HighWaterBoostFactorPPM = 1_500_000
	config.V2HighWaterBoostRecoverCoins = 1_000_000
	if err := validateRuleConfig(config); err != nil {
		t.Fatalf("valid v2 high-water boost rejected: %v", err)
	}
	// 恢复线等于阈值表示无滞回的普通开关语义，同样合法。
	config.V2HighWaterBoostRecoverCoins = config.V2HighWaterBoostThresholdCoins
	if err := validateRuleConfig(config); err != nil {
		t.Fatalf("recover==threshold should validate: %v", err)
	}
}

func TestValidateV2HighWaterBoostRejectsPartialOrInvalidTriples(t *testing.T) {
	cases := []struct {
		name      string
		threshold int64
		factor    int64
		recover   int64
		wantErr   string
	}{
		{name: "disabled must keep factor zero", threshold: 0, factor: 1_500_000, recover: 0, wantErr: "disabled"},
		{name: "disabled must keep recover zero", threshold: 0, factor: 0, recover: 1_000_000, wantErr: "disabled"},
		{name: "factor at 100% is meaningless", threshold: 2_000_000, factor: 1_000_000, recover: 1_000_000, wantErr: "factor"},
		{name: "factor above 1000% is a fat-finger guard", threshold: 2_000_000, factor: 10_000_001, recover: 1_000_000, wantErr: "factor"},
		{name: "recover must be positive", threshold: 2_000_000, factor: 1_500_000, recover: 0, wantErr: "recover"},
		{name: "recover cannot exceed threshold", threshold: 2_000_000, factor: 1_500_000, recover: 2_000_001, wantErr: "recover"},
	}
	for _, tc := range cases {
		config := legacyFixedV2Config()
		config.V2HighWaterBoostThresholdCoins = tc.threshold
		config.V2HighWaterBoostFactorPPM = tc.factor
		config.V2HighWaterBoostRecoverCoins = tc.recover
		if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), tc.wantErr) {
			t.Fatalf("%s: error=%v, want containing %q", tc.name, err, tc.wantErr)
		}
	}
}

func TestValidateDynamicV3RejectsV2HighWaterBoostFields(t *testing.T) {
	config := DefaultRuleConfig("lalu", "lucky")
	config.V2HighWaterBoostThresholdCoins = 2_000_000
	config.V2HighWaterBoostFactorPPM = 1_500_000
	config.V2HighWaterBoostRecoverCoins = 1_000_000
	if err := validateRuleConfig(config); err == nil || !strings.Contains(err.Error(), "fixed_v2 only") {
		t.Fatalf("dynamic_v3 with v2 boost fields error=%v, want fixed_v2-only rejection", err)
	}
}
