package mysql

import (
	"context"
	"sync"
	"testing"

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

func TestAdjustLuckyGiftPoolBalanceSeparatesStrategiesAndReplaysIdempotently(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")

	fixed := dynamicMySQLTestRule("shared-pool")
	fixed.StrategyVersion = domain.StrategyFixedV2
	if _, err := repo.PublishLuckyGiftRuleConfig(ctx, fixed, 1_700_010_000_000); err != nil {
		t.Fatalf("publish fixed config: %v", err)
	}
	dynamic := dynamicMySQLTestRule("shared-pool")
	if _, err := repo.PublishLuckyGiftRuleConfig(ctx, dynamic, 1_700_010_000_100); err != nil {
		t.Fatalf("publish dynamic config: %v", err)
	}

	fixedResult, err := repo.AdjustLuckyGiftPoolBalance(ctx, domain.PoolAdjustmentCommand{
		AppCode: "aslan", PoolID: "shared-pool", StrategyVersion: domain.StrategyFixedV2,
		AdjustmentID: "fixed-credit-1", Direction: domain.PoolAdjustmentIn, AmountCoins: 10,
		Reason: "legacy reserve", OperatorAdminID: 7,
	}, 1_700_010_001_000)
	if err != nil {
		t.Fatalf("credit fixed pool: %v", err)
	}
	if fixedResult.Pool.Balance != 3_835 || fixedResult.Pool.ReserveFloor != 210 {
		t.Fatalf("fixed pool snapshot=%+v, want old 3825/210 seed plus 10", fixedResult.Pool)
	}

	credit := domain.PoolAdjustmentCommand{
		AppCode: "aslan", PoolID: "shared-pool", StrategyVersion: domain.StrategyDynamicV3,
		AdjustmentID: "dynamic-credit-1", Direction: domain.PoolAdjustmentIn, AmountCoins: 100,
		Reason: "approved launch budget", OperatorAdminID: 7, RequestID: "request-1",
	}
	first, err := repo.AdjustLuckyGiftPoolBalance(ctx, credit, 1_700_010_002_000)
	if err != nil {
		t.Fatalf("credit dynamic pool: %v", err)
	}
	if first.Adjustment.BalanceBefore != 0 || first.Adjustment.BalanceAfter != 100 || first.Pool.ManualCreditTotal != 100 || first.Pool.TotalIn != 0 {
		t.Fatalf("dynamic credit result=%+v, want 0->100 isolated manual credit", first)
	}

	replayCommand := credit
	replayCommand.RequestID = "request-retry"
	replay, err := repo.AdjustLuckyGiftPoolBalance(ctx, replayCommand, 1_700_010_003_000)
	if err != nil {
		t.Fatalf("replay dynamic credit: %v", err)
	}
	if !replay.IdempotentReplay || replay.Adjustment.BalanceBefore != 0 || replay.Pool.Balance != 100 {
		t.Fatalf("replay result=%+v, want original snapshot", replay)
	}
	mismatch := credit
	mismatch.AmountCoins = 101
	if _, err := repo.AdjustLuckyGiftPoolBalance(ctx, mismatch, 1_700_010_004_000); !xerr.IsCode(err, xerr.IdempotencyConflict) {
		t.Fatalf("same ID with different payload error=%v, want idempotency conflict", err)
	}

	debit := domain.PoolAdjustmentCommand{
		AppCode: "aslan", PoolID: "shared-pool", StrategyVersion: domain.StrategyDynamicV3,
		AdjustmentID: "dynamic-debit-1", Direction: domain.PoolAdjustmentOut, AmountCoins: 30,
		Reason: "return unused budget", OperatorAdminID: 7,
	}
	debitResult, err := repo.AdjustLuckyGiftPoolBalance(ctx, debit, 1_700_010_005_000)
	if err != nil {
		t.Fatalf("debit dynamic pool: %v", err)
	}
	if debitResult.Pool.Balance != 70 || debitResult.Pool.ManualDebitTotal != 30 || debitResult.Pool.TotalOut != 0 {
		t.Fatalf("dynamic debit result=%+v, want manual 30 without business total_out", debitResult)
	}
	overDebit := debit
	overDebit.AdjustmentID = "dynamic-debit-over-reserve"
	overDebit.AmountCoins = 71
	if _, err := repo.AdjustLuckyGiftPoolBalance(ctx, overDebit, 1_700_010_006_000); !xerr.IsCode(err, xerr.Conflict) {
		t.Fatalf("over-reserve debit error=%v, want conflict", err)
	}

	// 八个并发重试共享一个 adjustment_id；唯一键和 FOR UPDATE 必须只让一次 10 金币入账。
	concurrent := credit
	concurrent.AdjustmentID = "dynamic-credit-concurrent"
	concurrent.AmountCoins = 10
	concurrent.Reason = "single concurrent grant"
	var wg sync.WaitGroup
	errorsByWorker := make([]error, 8)
	for index := range errorsByWorker {
		wg.Add(1)
		go func(worker int) {
			defer wg.Done()
			command := concurrent
			command.RequestID = "concurrent-request"
			_, errorsByWorker[worker] = repo.AdjustLuckyGiftPoolBalance(ctx, command, 1_700_010_007_000)
		}(index)
	}
	wg.Wait()
	for index, workerErr := range errorsByWorker {
		if workerErr != nil {
			t.Fatalf("concurrent worker %d: %v", index, workerErr)
		}
	}

	var dynamicBalance, dynamicManualCredit, dynamicManualDebit, dynamicTotalIn, dynamicTotalOut int64
	if err := schema.DB.QueryRow(`
		SELECT balance, manual_credit_total, manual_debit_total, total_in, total_out
		FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool_dynamic_v3' AND scope_id='shared-pool'`,
	).Scan(&dynamicBalance, &dynamicManualCredit, &dynamicManualDebit, &dynamicTotalIn, &dynamicTotalOut); err != nil {
		t.Fatalf("query dynamic pool: %v", err)
	}
	if dynamicBalance != 80 || dynamicManualCredit != 110 || dynamicManualDebit != 30 || dynamicTotalIn != 0 || dynamicTotalOut != 0 {
		t.Fatalf("dynamic ledger balance/credit/debit/in/out=%d/%d/%d/%d/%d, want 80/110/30/0/0", dynamicBalance, dynamicManualCredit, dynamicManualDebit, dynamicTotalIn, dynamicTotalOut)
	}
	var fixedBalance int64
	if err := schema.DB.QueryRow(`
		SELECT balance FROM lucky_pools WHERE app_code='aslan' AND scope_type='pool' AND scope_id='shared-pool'`,
	).Scan(&fixedBalance); err != nil || fixedBalance != 3_835 {
		t.Fatalf("fixed ledger balance=%d err=%v, want isolated 3835", fixedBalance, err)
	}

	items, err := repo.ListLuckyGiftPoolBalances(ctx, domain.PoolBalanceQuery{AppCode: "aslan", PoolID: "shared-pool"})
	if err != nil {
		t.Fatalf("list both strategy ledgers: %v", err)
	}
	if len(items) != 2 || items[0].StrategyVersion == items[1].StrategyVersion {
		t.Fatalf("strategy-separated pool list=%+v, want fixed_v2 and dynamic_v3", items)
	}
}

func TestAdjustLuckyGiftPoolBalanceRejectsUnconfiguredStrategy(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")
	_, err := repo.AdjustLuckyGiftPoolBalance(ctx, domain.PoolAdjustmentCommand{
		AppCode: "aslan", PoolID: "not-configured", StrategyVersion: domain.StrategyDynamicV3,
		AdjustmentID: "orphan-credit", Direction: domain.PoolAdjustmentIn, AmountCoins: 1,
		Reason: "must fail", OperatorAdminID: 7,
	}, 1_700_020_000_000)
	if !xerr.IsCode(err, xerr.NotFound) {
		t.Fatalf("unconfigured strategy error=%v, want not found", err)
	}
	var count int
	if scanErr := schema.DB.QueryRow(`SELECT COUNT(*) FROM lucky_pools WHERE app_code='aslan' AND scope_id='not-configured'`).Scan(&count); scanErr != nil || count != 0 {
		t.Fatalf("orphan pool count=%d err=%v, want 0", count, scanErr)
	}
}
