package mysql

import (
	"context"
	"database/sql"
	"os"
	"strings"
	"testing"

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

func TestSelectLuckyCandidateUsesSingleBasePoolCapacity(t *testing.T) {
	config := singlePoolSelectionConfig(5_000)
	candidate, limited, err := (&Repository{}).selectLuckyCandidate(config, domain.PoolNovice, luckyPool{
		ScopeType:    luckyBasePoolScopeType,
		ScopeID:      "default",
		Balance:      6_000,
		ReserveFloor: 1_000,
	})
	if err != nil {
		t.Fatalf("select candidate: %v", err)
	}
	if candidate.TierID != "novice_50x" || candidate.BaseReward != 5_000 {
		t.Fatalf("single base pool should allow payable high tier, got %+v", candidate)
	}
	if limited["pool_cap"] {
		t.Fatalf("pool_cap should not be set when the single base pool can pay the tier: %+v", limited)
	}
}

func TestSelectLuckyCandidateFiltersBySingleBasePoolCapacity(t *testing.T) {
	config := singlePoolSelectionConfig(5_000)
	candidate, limited, err := (&Repository{}).selectLuckyCandidate(config, domain.PoolNovice, luckyPool{
		ScopeType:    luckyBasePoolScopeType,
		ScopeID:      "default",
		Balance:      5_999,
		ReserveFloor: 1_000,
	})
	if err != nil {
		t.Fatalf("select candidate: %v", err)
	}
	if candidate.TierID != "no_payable_tier" || candidate.effectiveReward() != 0 {
		t.Fatalf("single base pool should reject an unaffordable tier, got %+v", candidate)
	}
	if !limited["pool_cap"] {
		t.Fatalf("pool_cap should explain the filtered tier: %+v", limited)
	}
}

func TestLuckyBasePoolInCreditsFullPoolRateToSinglePool(t *testing.T) {
	// 单池模型不再按 platform/room/gift 拆分入池金额；pool_rate 的结果全额进入 app+pool_id 基础池。
	if got := luckyBasePoolIn(domain.Config{PoolRatePPM: 990_000}, 100); got != 99 {
		t.Fatalf("luckyBasePoolIn()=%d, want 99", got)
	}
}

func TestLuckyAggregateDrawResultsKeepsOnlyWinningPositions(t *testing.T) {
	result := luckyAggregateDrawResults(domain.DrawCommand{CommandID: "cmd-combo"}, []domain.DrawResult{
		{DrawID: "draw-1", SelectedTierID: "2x", MultiplierPPM: 2_000_000, EffectiveRewardCoins: 20},
		{DrawID: "draw-2", SelectedTierID: "0x", MultiplierPPM: 0, EffectiveRewardCoins: 0},
		{DrawID: "draw-3", SelectedTierID: "5x", MultiplierPPM: 5_000_000, EffectiveRewardCoins: 50},
	})
	if len(result.Hits) != 2 {
		t.Fatalf("only winning sub-draws should be returned, got %+v", result.Hits)
	}
	if result.Hits[0].GiftIndex != 1 || result.Hits[0].DrawID != "draw-1" || result.Hits[1].GiftIndex != 3 || result.Hits[1].RewardCoins != 50 {
		t.Fatalf("winning positions mismatch: %+v", result.Hits)
	}
}

func TestMergeLuckyBasePoolsMigration(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",
	})

	// 迁移必须把同一 app_code+pool_id 下的新旧基础池水位合成一行；这里同时放入已迁移的 pool 行，
	// 用来证明脚本重跑时不会把旧水位重复累加，也不会把 room 维度继续留作开奖资金边界。
	_, err := schema.DB.Exec(`
		INSERT INTO lucky_pools (
		  app_code, scope_type, scope_id, balance, reserve_floor, total_in, total_out, created_at_ms, updated_at_ms
		) VALUES
		  ('aslan', 'platform', 'default', 100, 10, 1000, 100, 1, 1),
		  ('aslan', 'gift', 'default', 300, 30, 3000, 300, 1, 3),
		  ('aslan', 'room', 'default:101', 50, 5, 500, 50, 1, 4),
		  ('aslan', 'room', 'default:102', 70, 7, 700, 70, 1, 5),
		  ('aslan', 'pool', 'default', 20, 2, 200, 20, 1, 6),
		  ('aslan', 'platform', 'advanced', 9, 1, 90, 9, 1, 7)
	`)
	if err != nil {
		t.Fatalf("seed legacy pools: %v", err)
	}

	migrationPath := mysqlschema.InitDBPath(t, caller, "..", "..", "..", "deploy", "mysql", "migrations", "001_merge_lucky_base_pools.sql")
	runLuckyGiftSQLScript(t, schema.DB, migrationPath)
	assertMergedLuckyPool(t, schema.DB, "aslan", "default", 540, 54, 5400, 540, 1, 6)
	assertMergedLuckyPool(t, schema.DB, "aslan", "advanced", 9, 1, 90, 9, 1, 7)
	assertNoLegacyLuckyPools(t, schema.DB)

	// 迁移脚本会先按当前事实汇总到临时表，再删除参与汇总的行并重插新单池；二次执行应保持同样结果。
	runLuckyGiftSQLScript(t, schema.DB, migrationPath)
	assertMergedLuckyPool(t, schema.DB, "aslan", "default", 540, 54, 5400, 540, 1, 6)
	assertNoLegacyLuckyPools(t, schema.DB)
}

func singlePoolSelectionConfig(reward int64) domain.Config {
	return domain.Config{
		GiftPrice:       100,
		HighMultiplier:  10,
		PoolRatePPM:     990_000,
		InitialBasePool: 3_825,
		BasePoolReserve: 210,
		Tiers: []domain.Tier{{
			Pool:          domain.PoolNovice,
			TierID:        "novice_50x",
			RewardCoins:   reward,
			MultiplierPPM: 50_000_000,
			Weight:        1,
			HighWaterOnly: true,
			Enabled:       true,
		}},
	}
}

func runLuckyGiftSQLScript(t *testing.T, db *sql.DB, path string) {
	t.Helper()

	script, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read sql script %s: %v", path, err)
	}
	ctx := context.Background()
	conn, err := db.Conn(ctx)
	if err != nil {
		t.Fatalf("borrow mysql connection: %v", err)
	}
	defer conn.Close()
	for _, raw := range strings.Split(stripLuckyGiftSQLLineComments(string(script)), ";") {
		statement := strings.TrimSpace(raw)
		if statement == "" {
			continue
		}
		upper := strings.ToUpper(statement)
		if strings.HasPrefix(upper, "USE ") {
			continue
		}
		if _, err := conn.ExecContext(ctx, statement); err != nil {
			t.Fatalf("exec sql script statement failed: %v\nstatement:\n%s", err, statement)
		}
	}
}

func stripLuckyGiftSQLLineComments(script string) string {
	lines := strings.Split(script, "\n")
	kept := make([]string, 0, len(lines))
	for _, line := range lines {
		if strings.HasPrefix(strings.TrimSpace(line), "--") {
			continue
		}
		kept = append(kept, line)
	}
	return strings.Join(kept, "\n")
}

func assertMergedLuckyPool(t *testing.T, db *sql.DB, appCode string, poolID string, balance int64, reserveFloor int64, totalIn int64, totalOut int64, createdAtMS int64, updatedAtMS int64) {
	t.Helper()

	var gotBalance, gotReserve, gotIn, gotOut, gotCreated, gotUpdated int64
	err := db.QueryRow(`
		SELECT balance, reserve_floor, total_in, total_out, created_at_ms, updated_at_ms
		FROM lucky_pools
		WHERE app_code = ? AND scope_type = 'pool' AND scope_id = ?
	`, appCode, poolID).Scan(&gotBalance, &gotReserve, &gotIn, &gotOut, &gotCreated, &gotUpdated)
	if err != nil {
		t.Fatalf("query merged pool %s/%s: %v", appCode, poolID, err)
	}
	if gotBalance != balance || gotReserve != reserveFloor || gotIn != totalIn || gotOut != totalOut || gotCreated != createdAtMS || gotUpdated != updatedAtMS {
		t.Fatalf("merged pool %s/%s mismatch: balance=%d reserve=%d in=%d out=%d created=%d updated=%d",
			appCode, poolID, gotBalance, gotReserve, gotIn, gotOut, gotCreated, gotUpdated)
	}
}

func assertNoLegacyLuckyPools(t *testing.T, db *sql.DB) {
	t.Helper()

	var count int
	if err := db.QueryRow(`SELECT COUNT(*) FROM lucky_pools WHERE scope_type IN ('platform', 'room', 'gift')`).Scan(&count); err != nil {
		t.Fatalf("count legacy pools: %v", err)
	}
	if count != 0 {
		t.Fatalf("legacy platform/room/gift pools should be removed, got %d", count)
	}
}
