package mysql

import (
	"strings"
	"testing"

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

func TestDynamicLuckyGiftRequiresExplicitStableDeviceScope(t *testing.T) {
	for _, value := range []string{"", "   ", strings.Repeat("d", 129)} {
		if _, err := normalizeLuckyDynamicDeviceID(value); err == nil {
			t.Fatalf("dynamic_v3 accepted invalid device_id length=%d", len(value))
		}
	}
	if got, err := normalizeLuckyDynamicDeviceID(" device-auth-session-1 "); err != nil || got != "device-auth-session-1" {
		t.Fatalf("normalize explicit dynamic device scope got=%q err=%v", got, err)
	}
}

func TestExternalDeviceScopeUsesCallerBoundDeviceInsteadOfExternalUser(t *testing.T) {
	firstUser := domain.ExternalDrawCommand{AppCode: "aslan", ExternalUserID: "user-a", DeviceID: "shared-device"}
	secondUser := domain.ExternalDrawCommand{AppCode: "aslan", ExternalUserID: "user-b", DeviceID: "shared-device"}
	otherDevice := domain.ExternalDrawCommand{AppCode: "aslan", ExternalUserID: "user-a", DeviceID: "other-device"}
	firstScope := externalLuckyDeviceID(firstUser.AppCode, firstUser.DeviceID)
	if secondScope := externalLuckyDeviceID(secondUser.AppCode, secondUser.DeviceID); secondScope != firstScope {
		// 同物理设备上的多账号必须共享设备日上限，不能继续按 external_user_id 分裂。
		t.Fatalf("same external device produced different scopes: first=%q second=%q", firstScope, secondScope)
	}
	if otherScope := externalLuckyDeviceID(otherDevice.AppCode, otherDevice.DeviceID); otherScope == firstScope {
		t.Fatalf("different external devices collapsed to one scope: %q", otherScope)
	}

	legacyA := externalLuckyFixedCompatibleDeviceID(domain.ExternalDrawCommand{AppCode: "aslan", ExternalUserID: "legacy-a"})
	legacyB := externalLuckyFixedCompatibleDeviceID(domain.ExternalDrawCommand{AppCode: "aslan", ExternalUserID: "legacy-b"})
	if legacyA == legacyB {
		t.Fatalf("fixed_v2 legacy external users must retain their prior independent fallback scopes: %q", legacyA)
	}
	if historical := externalLuckyDeviceID("aslan", "legacy-a"); legacyA != historical {
		t.Fatalf("fixed_v2 legacy device scope changed: got=%q historical=%q", legacyA, historical)
	}
	if fixedWithNewField := externalLuckyFixedCompatibleDeviceID(firstUser); fixedWithNewField != externalLuckyDeviceID("aslan", "user-a") {
		t.Fatalf("fixed_v2 unexpectedly interpreted new device_id: %q", fixedWithNewField)
	}
}

func TestLuckyDynamicUnitFundSplitsConserveEveryDrawAndBatchBucket(t *testing.T) {
	tests := []struct {
		name       string
		spent      int64
		drawCount  int32
		totalSplit domain.StrategyFundSplit
	}{
		{
			name:      "image 100 coins across 99 draws",
			spent:     100,
			drawCount: 99,
			totalSplit: domain.StrategyFundSplit{
				PublicPoolCoins: 98, ProfitPoolCoins: 1, AnchorReturnCoins: 1,
			},
		},
		{
			name:      "990 coins keeps configured floor rounding totals",
			spent:     990,
			drawCount: 99,
			totalSplit: domain.StrategyFundSplit{
				PublicPoolCoins: 970, ProfitPoolCoins: 11, AnchorReturnCoins: 9,
			},
		},
		{
			name:      "uneven unit prices and configured buckets",
			spent:     101,
			drawCount: 6,
			totalSplit: domain.StrategyFundSplit{
				PublicPoolCoins: 90, ProfitPoolCoins: 5, AnchorReturnCoins: 6,
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			parts, err := luckyDynamicUnitFundSplits(test.spent, test.drawCount, test.totalSplit)
			if err != nil {
				t.Fatalf("split: %v", err)
			}
			if len(parts) != int(test.drawCount) {
				t.Fatalf("parts=%d want=%d", len(parts), test.drawCount)
			}
			var publicTotal, profitTotal, anchorTotal int64
			for index, part := range parts {
				unitSpent := luckyDrawUnitSpend(test.spent, test.drawCount, int32(index+1))
				if got := part.PublicPoolCoins + part.ProfitPoolCoins + part.AnchorReturnCoins; got != unitSpent {
					t.Fatalf("draw %d buckets=%d want unit spend=%d: %+v", index+1, got, unitSpent, part)
				}
				publicTotal += part.PublicPoolCoins
				profitTotal += part.ProfitPoolCoins
				anchorTotal += part.AnchorReturnCoins
			}
			if publicTotal != test.totalSplit.PublicPoolCoins || profitTotal != test.totalSplit.ProfitPoolCoins || anchorTotal != test.totalSplit.AnchorReturnCoins {
				t.Fatalf("batch totals=%d/%d/%d want=%+v", publicTotal, profitTotal, anchorTotal, test.totalSplit)
			}
		})
	}
}

func TestLuckyDynamicFundSplitUsesWalletAnchorIncome(t *testing.T) {
	config := domain.DefaultLuckyGiftStrategyConfig()
	if split, err := luckyDynamicFundSplit(100, 1, config); err != nil || split != (domain.StrategyFundSplit{PublicPoolCoins: 98, ProfitPoolCoins: 1, AnchorReturnCoins: 1}) {
		t.Fatalf("matching split=%+v err=%v", split, err)
	}
	if split, err := luckyDynamicFundSplit(100, 0, config); err != nil || split != (domain.StrategyFundSplit{PublicPoolCoins: 98, ProfitPoolCoins: 2, AnchorReturnCoins: 0}) {
		t.Fatalf("zero wallet anchor split=%+v err=%v", split, err)
	}
	if split, err := luckyDynamicFundSplit(100, 2, config); err != nil || split != (domain.StrategyFundSplit{PublicPoolCoins: 98, ProfitPoolCoins: 0, AnchorReturnCoins: 2}) {
		t.Fatalf("wallet-authoritative split=%+v err=%v", split, err)
	}
	if _, err := luckyDynamicFundSplit(100, 3, config); err == nil {
		t.Fatal("wallet anchor that exceeds the conserved remainder was accepted")
	}
}
