package mysql

import (
	"context"
	"testing"

	"hyapp/internal/testutil/mysqlschema"
	"hyapp/pkg/appcode"
	domain "hyapp/services/lucky-gift-service/internal/domain/luckygift"
)

// 回归：INSERT 列清单和占位符数量必须一致。拆分 lucky-gift-service 时列从 20 裁到 14，
// 占位符只裁到 15，导致所有规则发布在 MySQL 层直接失败并以 opaque internal error 暴露给运营后台。
func TestPublishLuckyGiftRuleConfigPersistsVersionsAndTiers(t *testing.T) {
	caller := mysqlschema.CallerFile(t, 1)
	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "LUCKY_GIFT_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     mysqlschema.InitDBPath(t, caller, "..", "..", "..", "deploy", "mysql", "initdb", "001_lucky_gift_service.sql"),
		DatabasePrefix: "hy_lucky_gift",
	})

	repo := &Repository{db: schema.DB}
	ctx := appcode.WithContext(context.Background(), "lalu")
	config := domain.RuleConfig{
		PoolID:                   "lucky",
		Enabled:                  true,
		TargetRTPPPM:             950_000,
		PoolRatePPM:              960_000,
		SettlementWindowWager:    1_000_000,
		ControlBandPPM:           10_000,
		GiftPriceReference:       100,
		NoviceMaxEquivalentDraws: 2_000,
		NormalMaxEquivalentDraws: 20_000,
		Stages: []domain.RuleStage{
			{Stage: domain.StageNovice, Tiers: []domain.RuleTier{
				{Stage: domain.StageNovice, TierID: "novice_1x", MultiplierPPM: 1_000_000, BaseWeightPPM: 1_000_000, Enabled: true},
			}},
		},
	}

	published, err := repo.PublishLuckyGiftRuleConfig(ctx, config, 1_700_000_000_000)
	if err != nil {
		t.Fatalf("publish first rule version: %v", err)
	}
	if published.RuleVersion != 1 {
		t.Fatalf("first publish rule_version = %d, want 1", published.RuleVersion)
	}
	if published.StrategyVersion != domain.StrategyFixedV2 {
		t.Fatalf("legacy request strategy = %q, want fixed_v2", published.StrategyVersion)
	}

	// 版本必须只增不改：同奖池再次发布要拿到 v2，且两版规则和奖档都能按最新版本读回。
	again, err := repo.PublishLuckyGiftRuleConfig(ctx, config, 1_700_000_100_000)
	if err != nil {
		t.Fatalf("publish second rule version: %v", err)
	}
	if again.RuleVersion != 2 {
		t.Fatalf("second publish rule_version = %d, want 2", again.RuleVersion)
	}

	configs, err := repo.ListLuckyGiftRuleConfigs(ctx, "lalu")
	if err != nil {
		t.Fatalf("list rule configs: %v", err)
	}
	if len(configs) != 1 {
		t.Fatalf("list should return the latest version per pool, got %d rows", len(configs))
	}
	latest := configs[0]
	if latest.PoolID != "lucky" || latest.RuleVersion != 2 || !latest.Enabled {
		t.Fatalf("latest config = %+v, want lucky v2 enabled", latest)
	}
	if len(latest.Stages) != 1 || len(latest.Stages[0].Tiers) != 1 || latest.Stages[0].Tiers[0].TierID != "novice_1x" {
		t.Fatalf("latest stages = %+v, want single novice tier novice_1x", latest.Stages)
	}

	// dynamic_v3 的 JSON 倍率、阶段充值阈值和金额风控必须随不可变版本完整往返；否则服务重启后会静默退回默认策略。
	dynamic := config
	dynamic.StrategyVersion = domain.StrategyDynamicV3
	dynamic.ProfitRatePPM = 10_000
	dynamic.AnchorRatePPM = 10_000
	dynamic.InitialPoolCoins = 0
	dynamic.LossStreakGuarantee = 5
	dynamic.LowWatermarkCoins = 10_000_000
	dynamic.LowWaterNonzeroFactorPPM = 700_000
	dynamic.HighWatermarkCoins = 20_000_000
	dynamic.HighWaterNonzeroFactorPPM = 1_300_000
	dynamic.RechargeBoostWindowMS = 300_000
	dynamic.RechargeBoostFactorPPM = 1_100_000
	dynamic.JackpotMultiplierPPMs = []int64{200_000_000, 500_000_000, 1_000_000_000}
	dynamic.JackpotGlobalRTPMaxPPM = 980_000
	dynamic.JackpotUserDayRTPMaxPPM = 960_000
	dynamic.JackpotUser72hRTPMaxPPM = 960_000
	dynamic.JackpotSpendThresholdCoins = 5_000
	dynamic.MaxJackpotHitsPerUserDay = 5
	dynamic.MaxSinglePayout = 1_000_000
	dynamic.UserHourlyPayoutCap = 2_000_000
	dynamic.UserDailyPayoutCap = 3_000_000
	dynamic.DeviceDailyPayoutCap = 4_000_000
	dynamic.RoomHourlyPayoutCap = 5_000_000
	dynamic.AnchorDailyPayoutCap = 6_000_000
	dynamic.Stages[0].MinRecharge7DCoins = 1
	dynamic.Stages[0].MinRecharge30DCoins = 2

	third, err := repo.PublishLuckyGiftRuleConfig(ctx, dynamic, 1_700_000_200_000)
	if err != nil {
		t.Fatalf("publish dynamic rule version: %v", err)
	}
	if third.RuleVersion != 3 {
		t.Fatalf("dynamic rule_version = %d, want 3", third.RuleVersion)
	}
	loaded, exists, err := repo.GetLuckyGiftRuleConfig(ctx, "lucky")
	if err != nil || !exists {
		t.Fatalf("get dynamic rule: exists=%v err=%v", exists, err)
	}
	if loaded.StrategyVersion != domain.StrategyDynamicV3 || loaded.InitialPoolCoins != 0 || loaded.AnchorDailyPayoutCap != 6_000_000 {
		t.Fatalf("dynamic scalar fields were not persisted: %+v", loaded)
	}
	if len(loaded.JackpotMultiplierPPMs) != 3 || loaded.JackpotMultiplierPPMs[2] != 1_000_000_000 {
		t.Fatalf("jackpot multipliers = %v, want [200x 500x 1000x] ppm", loaded.JackpotMultiplierPPMs)
	}
	if len(loaded.Stages) != 1 || loaded.Stages[0].MinRecharge7DCoins != 1 || loaded.Stages[0].MinRecharge30DCoins != 2 {
		t.Fatalf("dynamic stage thresholds were not persisted: %+v", loaded.Stages)
	}
	var dynamicBalance int64
	if err := schema.DB.QueryRow(`
		SELECT balance FROM lucky_pools
		WHERE app_code='lalu' AND scope_type='pool_dynamic_v3' AND scope_id='lucky'`).Scan(&dynamicBalance); err != nil || dynamicBalance != 0 {
		t.Fatalf("first dynamic publish balance=%d err=%v, want materialized zero ledger", dynamicBalance, err)
	}
	if _, err := schema.DB.Exec(`
		UPDATE lucky_pools SET balance=777, manual_credit_total=777
		WHERE app_code='lalu' AND scope_type='pool_dynamic_v3' AND scope_id='lucky'`); err != nil {
		t.Fatalf("fund dynamic ledger before republish: %v", err)
	}
	if _, err := repo.PublishLuckyGiftRuleConfig(ctx, dynamic, 1_700_000_300_000); err != nil {
		t.Fatalf("publish later dynamic version: %v", err)
	}
	if err := schema.DB.QueryRow(`
		SELECT balance FROM lucky_pools
		WHERE app_code='lalu' AND scope_type='pool_dynamic_v3' AND scope_id='lucky'`).Scan(&dynamicBalance); err != nil || dynamicBalance != 777 {
		t.Fatalf("later dynamic publish balance=%d err=%v, want existing 777 preserved", dynamicBalance, err)
	}
}

func TestDynamicStrategyMigrationIsIdempotent(t *testing.T) {
	caller := mysqlschema.CallerFile(t, 1)
	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "LUCKY_GIFT_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     mysqlschema.InitDBPath(t, caller, "..", "..", "..", "deploy", "mysql", "initdb", "001_lucky_gift_service.sql"),
		DatabasePrefix: "hy_lucky_gift",
	})
	migrationPath := mysqlschema.InitDBPath(t, caller, "..", "..", "..", "deploy", "mysql", "migrations", "004_dynamic_strategy.sql")
	// initdb 已包含最终结构，004 连续运行两次仍必须成功，覆盖全新环境与线上重复发布 migration 两条路径。
	runLuckyGiftSQLScript(t, schema.DB, migrationPath)
	runLuckyGiftSQLScript(t, schema.DB, migrationPath)
	// 007 must also work on a pre-dual-strategy schema and remain replay-safe. Dropping only the new
	// constant-default column reproduces that shape without scanning or rewriting any user row.
	if _, err := schema.DB.Exec(`ALTER TABLE lucky_user_states DROP COLUMN pending_spend_jackpot_tokens, ALGORITHM=INSTANT`); err != nil {
		t.Fatalf("prepare pre-007 schema: %v", err)
	}
	spendTokenMigrationPath := mysqlschema.InitDBPath(t, caller, "..", "..", "..", "deploy", "mysql", "migrations", "007_dual_jackpot_spend_tokens.sql")
	runLuckyGiftSQLScript(t, schema.DB, spendTokenMigrationPath)
	runLuckyGiftSQLScript(t, schema.DB, spendTokenMigrationPath)

	for _, table := range []string{
		"lucky_risk_counters", "lucky_user_rtp_hour_buckets", "lucky_user_rtp_boundary_events",
		"lucky_user_strategy_days", "lucky_gift_command_locks", "external_lucky_gift_draw_items", "external_lucky_gift_request_locks",
	} {
		var count int
		if err := schema.DB.QueryRow(`
			SELECT COUNT(*) FROM information_schema.TABLES
			WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?`, table).Scan(&count); err != nil || count != 1 {
			t.Fatalf("migration table %s: count=%d err=%v", table, count, err)
		}
	}
	for _, column := range []struct {
		table     string
		name      string
		minLength int64
	}{
		{table: "lucky_user_states", name: "pending_jackpot_tokens"},
		{table: "lucky_user_states", name: "pending_spend_jackpot_tokens"},
		{table: "lucky_rtp_windows", name: "scope_id", minLength: 128},
		{table: "lucky_risk_counters", name: "scope_id", minLength: 255},
		{table: "lucky_gift_command_locks", name: "command_id", minLength: 128},
	} {
		var count, length int64
		if err := schema.DB.QueryRow(`
			SELECT COUNT(*), COALESCE(MAX(CHARACTER_MAXIMUM_LENGTH), 0)
			FROM information_schema.COLUMNS
			WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
			column.table, column.name,
		).Scan(&count, &length); err != nil || count != 1 || (column.minLength > 0 && length < column.minLength) {
			t.Fatalf("migration column %s.%s count=%d length=%d err=%v", column.table, column.name, count, length, err)
		}
	}
	// 旧发布器只写 v2 原列；数据库默认值和扫描器必须共同把该行解释为 fixed_v2，而不是要求动态字段。
	if _, err := schema.DB.Exec(`
		INSERT INTO lucky_gift_rule_versions (
		  app_code, pool_id, rule_version, enabled, target_rtp_ppm, pool_rate_ppm,
		  settlement_window_wager, control_band_ppm, gift_price_reference,
		  novice_max_equivalent_draws, normal_max_equivalent_draws,
		  effective_from_ms, created_by_admin_id, created_at_ms
		) VALUES ('legacy', 'lucky', 1, 1, 950000, 960000, 1000000, 10000, 100, 2000, 20000, 1, 7, 1)`); err != nil {
		t.Fatalf("insert pre-dynamic rule row: %v", err)
	}
	repo := &Repository{db: schema.DB}
	legacy, exists, err := repo.GetLuckyGiftRuleConfig(appcode.WithContext(context.Background(), "legacy"), "lucky")
	if err != nil || !exists {
		t.Fatalf("read pre-dynamic rule row: exists=%v err=%v", exists, err)
	}
	if legacy.StrategyVersion != domain.StrategyFixedV2 || len(legacy.JackpotMultiplierPPMs) != 0 {
		t.Fatalf("pre-dynamic row decoded as strategy=%q jackpot=%v, want fixed_v2 and empty jackpot", legacy.StrategyVersion, legacy.JackpotMultiplierPPMs)
	}
}

func TestLuckyPoolBalanceMigrationMaterializesLatestDynamicV3AtZeroWithoutReset(t *testing.T) {
	caller := mysqlschema.CallerFile(t, 1)
	schema := mysqlschema.New(t, mysqlschema.Config{
		EnvVar:         "LUCKY_GIFT_SERVICE_MYSQL_TEST_DSN",
		InitDBPath:     mysqlschema.InitDBPath(t, caller, "..", "..", "..", "deploy", "mysql", "initdb", "001_lucky_gift_service.sql"),
		DatabasePrefix: "hy_lucky_gift",
	})
	repo := &Repository{db: schema.DB}
	ctx := appcode.WithContext(context.Background(), "aslan")
	rule := dynamicMySQLTestRule("migration-v3")
	if _, err := repo.PublishLuckyGiftRuleConfig(ctx, rule, 1_700_001_000_000); err != nil {
		t.Fatalf("publish dynamic config: %v", err)
	}
	// 模拟现网已经发布的旧 V3 快照：配置里仍残留 1,000,000，但 005 必须忽略它并创建零余额新 scope。
	if _, err := schema.DB.Exec(`UPDATE lucky_gift_rule_versions SET initial_pool_coins=1000000 WHERE app_code='aslan' AND pool_id='migration-v3'`); err != nil {
		t.Fatalf("seed legacy dynamic initial_pool_coins: %v", err)
	}
	if _, err := schema.DB.Exec(`
		INSERT INTO lucky_pools (
		  app_code, scope_type, scope_id, balance, reserve_floor, total_in, total_out,
		  profit_total, anchor_income_total, initial_seed_total, created_at_ms, updated_at_ms
		) VALUES ('aslan', 'pool', 'migration-v3', 888, 88, 1000, 112, 0, 0, 888, 1, 2)`); err != nil {
		t.Fatalf("seed legacy fixed scope: %v", err)
	}
	if _, err := schema.DB.Exec(`DELETE FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool_dynamic_v3' AND scope_id='migration-v3'`); err != nil {
		t.Fatalf("simulate pre-005 database: %v", err)
	}
	migrationPath := mysqlschema.InitDBPath(t, caller, "..", "..", "..", "deploy", "mysql", "migrations", "005_lucky_pool_balance.sql")
	runLuckyGiftSQLScript(t, schema.DB, migrationPath)
	var balance int64
	if err := schema.DB.QueryRow(`SELECT balance FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool_dynamic_v3' AND scope_id='migration-v3'`).Scan(&balance); err != nil || balance != 0 {
		t.Fatalf("005 materialized balance=%d err=%v, want 0", balance, err)
	}
	var legacyBalance int64
	if err := schema.DB.QueryRow(`SELECT balance FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool' AND scope_id='migration-v3'`).Scan(&legacyBalance); err != nil || legacyBalance != 888 {
		t.Fatalf("005 changed legacy fixed scope balance=%d err=%v, want 888", legacyBalance, err)
	}
	if _, err := schema.DB.Exec(`UPDATE lucky_pools SET balance=321, manual_credit_total=321 WHERE app_code='aslan' AND scope_type='pool_dynamic_v3' AND scope_id='migration-v3'`); err != nil {
		t.Fatalf("fund migrated pool: %v", err)
	}
	runLuckyGiftSQLScript(t, schema.DB, migrationPath)
	if err := schema.DB.QueryRow(`SELECT balance FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool_dynamic_v3' AND scope_id='migration-v3'`).Scan(&balance); err != nil || balance != 321 {
		t.Fatalf("rerun 005 balance=%d err=%v, want existing 321 preserved", balance, err)
	}
	if err := schema.DB.QueryRow(`SELECT balance FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool' AND scope_id='migration-v3'`).Scan(&legacyBalance); err != nil || legacyBalance != 888 {
		t.Fatalf("rerun 005 changed legacy fixed scope balance=%d err=%v, want 888", legacyBalance, err)
	}
}
