package service

import (
	"context"
	"errors"
	"math"
	"strconv"
	"testing"
	"time"

	"github.com/Wei-Shaw/sub2api/internal/config"
	"github.com/stretchr/testify/require"
)

type upstreamCostTrackingConcurrencyCache struct {
	ConcurrencyCache
	loadMap       map[int64]*AccountLoadInfo
	acquireLimits map[int64][]int
	releases      map[int64]int
	rejectAcquire bool
}

func (c *upstreamCostTrackingConcurrencyCache) AcquireAccountSlot(_ context.Context, accountID int64, maxConcurrency int, _ string) (bool, error) {
	if c.acquireLimits == nil {
		c.acquireLimits = make(map[int64][]int)
	}
	c.acquireLimits[accountID] = append(c.acquireLimits[accountID], maxConcurrency)
	return !c.rejectAcquire, nil
}

func (c *upstreamCostTrackingConcurrencyCache) ReleaseAccountSlot(_ context.Context, accountID int64, _ string) error {
	if c.releases == nil {
		c.releases = make(map[int64]int)
	}
	c.releases[accountID]++
	return nil
}

func (c *upstreamCostTrackingConcurrencyCache) GetAccountsLoadBatch(_ context.Context, accounts []AccountWithConcurrency) (map[int64]*AccountLoadInfo, error) {
	out := make(map[int64]*AccountLoadInfo, len(accounts))
	for _, account := range accounts {
		if load := c.loadMap[account.ID]; load != nil {
			copied := *load
			out[account.ID] = &copied
		}
	}
	return out, nil
}

func (c *upstreamCostTrackingConcurrencyCache) limits(accountID int64) []int {
	return append([]int(nil), c.acquireLimits[accountID]...)
}

func (c *upstreamCostTrackingConcurrencyCache) releaseCount(accountID int64) int {
	return c.releases[accountID]
}

func (c *upstreamCostTrackingConcurrencyCache) totalAcquires() int {
	total := 0
	for _, limits := range c.acquireLimits {
		total += len(limits)
	}
	return total
}

type upstreamCostCountingAccountRepo struct {
	AccountRepository
	accounts map[int64]*Account
	getCalls int
}

func (r *upstreamCostCountingAccountRepo) GetByID(_ context.Context, accountID int64) (*Account, error) {
	r.getCalls++
	account := r.accounts[accountID]
	if account == nil {
		return nil, errors.New("account not found")
	}
	cloned := *account
	return &cloned, nil
}

func (r *upstreamCostCountingAccountRepo) calls() int {
	return r.getCalls
}

func upstreamCostTestAccount(id int64, status string, rate float64, receivedAt time.Time, interval time.Duration) *Account {
	return &Account{
		ID:       id,
		Platform: PlatformOpenAI,
		Type:     AccountTypeAPIKey,
		Extra: map[string]any{
			UpstreamBillingProbeExtraKey: map[string]any{
				"status": status,
				"data": map[string]any{
					"billing_scope":             "token",
					"resolved_rate_multiplier":  rate,
					"peak_rate_enabled":         false,
					"effective_rate_multiplier": rate,
				},
				"received_at":     receivedAt.UTC().Format(time.RFC3339Nano),
				"fresh_until":     receivedAt.Add(2 * interval).UTC().Format(time.RFC3339Nano),
				"last_attempt_at": receivedAt.UTC().Format(time.RFC3339Nano),
				"next_probe_at":   receivedAt.Add(interval).UTC().Format(time.RFC3339Nano),
			},
		},
	}
}

func upstreamCostTestOAuthAccount(id int64) *Account {
	return &Account{ID: id, Platform: PlatformOpenAI, Type: AccountTypeOAuth}
}

func TestAdvancedCostSchedulerUsesTopKOverflowWhenPreferredAccountIsKnownFull(t *testing.T) {
	resetOpenAIAdvancedSchedulerSettingCacheForTest()
	defer resetOpenAIAdvancedSchedulerSettingCacheForTest()

	now := time.Now()
	cheap := upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute)
	expensive := upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.8, now.Add(-time.Minute), 30*time.Minute)
	for _, account := range []*Account{cheap, expensive} {
		account.Status = StatusActive
		account.Schedulable = true
		account.Concurrency = 1
	}
	cache := &upstreamCostTrackingConcurrencyCache{loadMap: map[int64]*AccountLoadInfo{
		cheap.ID:     {AccountID: cheap.ID, CurrentConcurrency: 1, LoadRate: 100},
		expensive.ID: {AccountID: expensive.ID},
	}}
	cfg := &config.Config{}
	cfg.Gateway.OpenAIWS.LBTopK = 1
	cfg.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost = 1
	svc := &OpenAIGatewayService{
		accountRepo:        schedulerTestOpenAIAccountRepo{accounts: []Account{*cheap, *expensive}},
		cfg:                cfg,
		rateLimitService:   newOpenAIAdvancedSchedulerRateLimitService("true"),
		concurrencyService: NewConcurrencyService(cache),
	}
	groupID := int64(1)

	selection, _, err := svc.SelectAccountWithScheduler(context.Background(), &groupID, "", "", "gpt-test", nil, OpenAIUpstreamTransportAny, false)
	require.NoError(t, err)
	require.Equal(t, expensive.ID, selection.Account.ID)
	require.Empty(t, cache.limits(cheap.ID))
	require.Equal(t, []int{1}, cache.limits(expensive.ID))
	selection.ReleaseFunc()
}

func TestAdvancedSchedulerCapsRejectedCostOverflowAcquires(t *testing.T) {
	selectionOrder := make([]openAIAccountCandidateScore, 0, 15_000)
	for id := int64(1); id <= 15_000; id++ {
		account := &Account{ID: id, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Status: StatusActive, Schedulable: true, Concurrency: 1}
		selectionOrder = append(selectionOrder, openAIAccountCandidateScore{
			account: account, loadInfo: &AccountLoadInfo{AccountID: id}, loadKnown: false,
		})
	}
	cache := &upstreamCostTrackingConcurrencyCache{rejectAcquire: true}
	scheduler := &defaultOpenAIAccountScheduler{service: &OpenAIGatewayService{
		concurrencyService: NewConcurrencyService(cache),
	}}

	selection, _, err := scheduler.tryAcquireOpenAISelectionOrder(
		context.Background(), OpenAIAccountScheduleRequest{Platform: PlatformOpenAI}, selectionOrder,
	)

	require.NoError(t, err)
	require.Nil(t, selection)
	require.Equal(t, openAIAccountSelectionProbeLimit, cache.totalAcquires())
}

func TestOpenAICostOverflowExpandedOnlyWhenCostAddsCandidates(t *testing.T) {
	candidates := []openAIAccountCandidateScore{
		{account: &Account{ID: 1, Extra: map[string]any{"openai_compact_supported": true}}},
		{account: &Account{ID: 2}},
	}
	plan := openAIAccountLoadPlan{candidates: candidates, topK: 1, includeOverflowFallback: true}
	require.True(t, openAICostOverflowExpanded(OpenAIAccountScheduleRequest{}, plan))
	require.False(t, openAICostOverflowExpanded(OpenAIAccountScheduleRequest{RequireCompact: true}, plan),
		"one candidate per compact tier does not expand either tier's top-k")
	plan.topK = len(candidates)
	require.False(t, openAICostOverflowExpanded(OpenAIAccountScheduleRequest{}, plan))
	plan.includeOverflowFallback = false
	plan.topK = 1
	require.False(t, openAICostOverflowExpanded(OpenAIAccountScheduleRequest{}, plan))
}

func TestAdvancedSchedulerKnownFullOverflowStillFindsAvailableAccount(t *testing.T) {
	selectionOrder := make([]openAIAccountCandidateScore, 0, openAIAccountSelectionProbeLimit+2)
	for id := int64(1); id <= openAIAccountSelectionProbeLimit+1; id++ {
		account := &Account{ID: id, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Status: StatusActive, Schedulable: true, Concurrency: 1}
		selectionOrder = append(selectionOrder, openAIAccountCandidateScore{
			account:   account,
			loadInfo:  &AccountLoadInfo{AccountID: id, CurrentConcurrency: 1, LoadRate: 100},
			loadKnown: true,
		})
	}
	availableID := int64(openAIAccountSelectionProbeLimit + 2)
	available := &Account{ID: availableID, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Status: StatusActive, Schedulable: true, Concurrency: 1}
	selectionOrder = append(selectionOrder, openAIAccountCandidateScore{
		account: available, loadInfo: &AccountLoadInfo{AccountID: availableID}, loadKnown: true,
	})
	cache := &upstreamCostTrackingConcurrencyCache{}
	scheduler := &defaultOpenAIAccountScheduler{service: &OpenAIGatewayService{
		concurrencyService: NewConcurrencyService(cache),
	}}

	selection, _, err := scheduler.tryAcquireOpenAISelectionOrder(
		context.Background(), OpenAIAccountScheduleRequest{Platform: PlatformOpenAI}, selectionOrder,
	)

	require.NoError(t, err)
	require.NotNil(t, selection)
	require.Equal(t, availableID, selection.Account.ID)
	require.Equal(t, 1, cache.totalAcquires())
	selection.ReleaseFunc()
}

func TestAdvancedSchedulerSharesProbeBudgetWithFallbackDBRechecks(t *testing.T) {
	const size = 15_000
	latestAccounts := make(map[int64]*Account, size)
	snapshotAccounts := make(map[int64]*Account, size)
	selectionOrder := make([]openAIAccountCandidateScore, 0, size)
	for id := int64(1); id <= size; id++ {
		stale := &Account{ID: id, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Status: StatusActive, Schedulable: true, Concurrency: 1}
		latest := *stale
		latest.Status = StatusDisabled
		snapshotAccounts[id] = stale
		latestAccounts[id] = &latest
		selectionOrder = append(selectionOrder, openAIAccountCandidateScore{
			account: stale, loadInfo: &AccountLoadInfo{AccountID: id}, loadKnown: false,
		})
	}
	repo := &upstreamCostCountingAccountRepo{accounts: latestAccounts}
	cache := &upstreamCostTrackingConcurrencyCache{}
	scheduler := &defaultOpenAIAccountScheduler{service: &OpenAIGatewayService{
		accountRepo:        repo,
		schedulerSnapshot:  &SchedulerSnapshotService{cache: &openAISnapshotCacheStub{accountsByID: snapshotAccounts}},
		concurrencyService: NewConcurrencyService(cache),
	}}
	budget := newOpenAISelectionProbeBudget()
	budget.enableLimit()
	req := OpenAIAccountScheduleRequest{Platform: PlatformOpenAI}

	selection, _, err := scheduler.tryAcquireOpenAISelectionOrderWithBudget(context.Background(), req, selectionOrder, budget)
	require.NoError(t, err)
	require.Nil(t, selection)
	selection, _, _, _, err = scheduler.finishLoadBalanceSelectionFallback(
		context.Background(), req, openAIAccountLoadSelectionAttempt{selectionOrder: selectionOrder}, budget, openAISelectionFilterStats{},
	)

	require.Error(t, err)
	require.Nil(t, selection)
	require.Equal(t, openAIAccountSelectionProbeLimit, cache.totalAcquires())
	require.Equal(t, openAIAccountSelectionProbeLimit, repo.calls())
}

func TestAdvancedCostSchedulerKeepsCompactSupportedOverflowAheadOfUnknown(t *testing.T) {
	resetOpenAIAdvancedSchedulerSettingCacheForTest()
	defer resetOpenAIAdvancedSchedulerSettingCacheForTest()

	now := time.Now()
	preferred := upstreamCostTestAccount(11, UpstreamBillingProbeStatusOK, 0.01, now.Add(-time.Minute), 30*time.Minute)
	overflow := upstreamCostTestAccount(12, UpstreamBillingProbeStatusOK, 0.1, now.Add(-time.Minute), 30*time.Minute)
	unknown := upstreamCostTestAccount(13, UpstreamBillingProbeStatusOK, 0.001, now.Add(-time.Minute), 30*time.Minute)
	preferred.Extra["openai_compact_supported"] = true
	overflow.Extra["openai_compact_supported"] = true
	for _, account := range []*Account{preferred, overflow, unknown} {
		account.Status = StatusActive
		account.Schedulable = true
		account.Concurrency = 1
	}
	cache := &upstreamCostTrackingConcurrencyCache{loadMap: map[int64]*AccountLoadInfo{
		preferred.ID: {AccountID: preferred.ID, CurrentConcurrency: 1, LoadRate: 100},
		overflow.ID:  {AccountID: overflow.ID},
		unknown.ID:   {AccountID: unknown.ID},
	}}
	cfg := &config.Config{}
	cfg.Gateway.OpenAIWS.LBTopK = 1
	cfg.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost = 1
	svc := &OpenAIGatewayService{
		accountRepo:        schedulerTestOpenAIAccountRepo{accounts: []Account{*preferred, *overflow, *unknown}},
		cfg:                cfg,
		rateLimitService:   newOpenAIAdvancedSchedulerRateLimitService("true"),
		concurrencyService: NewConcurrencyService(cache),
	}
	groupID := int64(1)

	selection, _, err := svc.SelectAccountWithScheduler(context.Background(), &groupID, "", "", "gpt-test", nil, OpenAIUpstreamTransportAny, true)
	require.NoError(t, err)
	require.Equal(t, overflow.ID, selection.Account.ID)
	require.Empty(t, cache.limits(preferred.ID))
	require.Equal(t, []int{1}, cache.limits(overflow.ID))
	require.Empty(t, cache.limits(unknown.ID))
	selection.ReleaseFunc()
}

func TestAdvancedSchedulerUnknownLoadFailsOpen(t *testing.T) {
	account := &Account{ID: 21, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Status: StatusActive, Schedulable: true, Concurrency: 1}
	cache := &upstreamCostTrackingConcurrencyCache{}
	scheduler := &defaultOpenAIAccountScheduler{service: &OpenAIGatewayService{concurrencyService: NewConcurrencyService(cache)}}

	selection, _, err := scheduler.tryAcquireOpenAISelectionOrder(context.Background(), OpenAIAccountScheduleRequest{Platform: PlatformOpenAI}, []openAIAccountCandidateScore{{
		account: account, loadInfo: &AccountLoadInfo{AccountID: account.ID, CurrentConcurrency: 99}, loadKnown: false,
	}})
	require.NoError(t, err)
	require.NotNil(t, selection)
	require.Equal(t, []int{1}, cache.limits(account.ID))
	selection.ReleaseFunc()
}

func TestAdvancedSchedulerReleasesSlotWhenDBDisablesCandidate(t *testing.T) {
	stale := &Account{ID: 31, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Status: StatusActive, Schedulable: true, Concurrency: 1}
	backup := &Account{ID: 32, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Status: StatusActive, Schedulable: true, Concurrency: 1}
	disabled := *stale
	disabled.Status = StatusDisabled
	repo := &upstreamCostCountingAccountRepo{accounts: map[int64]*Account{stale.ID: &disabled, backup.ID: backup}}
	snapshot := &openAISnapshotCacheStub{accountsByID: map[int64]*Account{stale.ID: stale, backup.ID: backup}}
	cache := &upstreamCostTrackingConcurrencyCache{}
	scheduler := &defaultOpenAIAccountScheduler{service: &OpenAIGatewayService{
		accountRepo:        repo,
		schedulerSnapshot:  &SchedulerSnapshotService{cache: snapshot},
		concurrencyService: NewConcurrencyService(cache),
	}}

	selection, _, err := scheduler.tryAcquireOpenAISelectionOrder(context.Background(), OpenAIAccountScheduleRequest{Platform: PlatformOpenAI}, []openAIAccountCandidateScore{
		{account: stale, loadInfo: &AccountLoadInfo{AccountID: stale.ID}, loadKnown: true},
		{account: backup, loadInfo: &AccountLoadInfo{AccountID: backup.ID}, loadKnown: true},
	})
	require.NoError(t, err)
	require.Equal(t, backup.ID, selection.Account.ID)
	require.Equal(t, 1, cache.releaseCount(stale.ID))
	selection.ReleaseFunc()
}

func TestAdvancedSchedulerReacquiresOnceWhenDBConcurrencyChanges(t *testing.T) {
	stale := &Account{ID: 41, Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Status: StatusActive, Schedulable: true, Concurrency: 10}
	latest := *stale
	latest.Concurrency = 1
	repo := &upstreamCostCountingAccountRepo{accounts: map[int64]*Account{stale.ID: &latest}}
	snapshot := &openAISnapshotCacheStub{accountsByID: map[int64]*Account{stale.ID: stale}}
	cache := &upstreamCostTrackingConcurrencyCache{}
	scheduler := &defaultOpenAIAccountScheduler{service: &OpenAIGatewayService{
		accountRepo:        repo,
		schedulerSnapshot:  &SchedulerSnapshotService{cache: snapshot},
		concurrencyService: NewConcurrencyService(cache),
	}}

	selection, _, err := scheduler.tryAcquireOpenAISelectionOrder(context.Background(), OpenAIAccountScheduleRequest{Platform: PlatformOpenAI}, []openAIAccountCandidateScore{{
		account: stale, loadInfo: &AccountLoadInfo{AccountID: stale.ID}, loadKnown: true,
	}})
	require.NoError(t, err)
	require.Equal(t, 1, selection.Account.Concurrency)
	require.Equal(t, []int{10, 1}, cache.limits(stale.ID))
	require.Equal(t, 1, cache.releaseCount(stale.ID))
	selection.ReleaseFunc()
}

func TestAdvancedSchedulerKnownFullPoolsDoNotRecheckDB(t *testing.T) {
	for _, size := range []int{100, 15_000} {
		t.Run(strconv.Itoa(size), func(t *testing.T) {
			accounts := make(map[int64]*Account, size)
			selectionOrder := make([]openAIAccountCandidateScore, 0, size)
			for i := 1; i <= size; i++ {
				account := &Account{ID: int64(i), Platform: PlatformOpenAI, Type: AccountTypeAPIKey, Status: StatusActive, Schedulable: true, Concurrency: 1}
				accounts[account.ID] = account
				selectionOrder = append(selectionOrder, openAIAccountCandidateScore{
					account:   account,
					loadInfo:  &AccountLoadInfo{AccountID: account.ID, CurrentConcurrency: 1, LoadRate: 100},
					loadKnown: true,
				})
			}
			repo := &upstreamCostCountingAccountRepo{accounts: accounts}
			cache := &upstreamCostTrackingConcurrencyCache{}
			scheduler := &defaultOpenAIAccountScheduler{service: &OpenAIGatewayService{
				accountRepo:        repo,
				schedulerSnapshot:  &SchedulerSnapshotService{cache: &openAISnapshotCacheStub{accountsByID: accounts}},
				concurrencyService: NewConcurrencyService(cache),
			}}

			selection, _, err := scheduler.tryAcquireOpenAISelectionOrder(context.Background(), OpenAIAccountScheduleRequest{Platform: PlatformOpenAI}, selectionOrder)
			require.NoError(t, err)
			require.Nil(t, selection)
			require.Zero(t, repo.calls())
			require.Zero(t, cache.totalAcquires())
		})
	}
}

func TestOpenAIFreshUpstreamBillingRateRecomputesPeakAtSelectionTime(t *testing.T) {
	receivedAt := time.Date(2026, 7, 13, 17, 30, 0, 0, time.UTC)
	account := upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.4, receivedAt, time.Hour)
	snapshot, ok := account.Extra[UpstreamBillingProbeExtraKey].(map[string]any)
	require.True(t, ok)
	snapshot["data"] = map[string]any{
		"billing_scope":             "token",
		"resolved_rate_multiplier":  0.4,
		"peak_rate_enabled":         true,
		"peak_start":                "09:00",
		"peak_end":                  "18:00",
		"peak_rate_multiplier":      2.0,
		"applied_peak_multiplier":   2.0,
		"effective_rate_multiplier": 0.8,
		"timezone":                  "UTC",
	}

	duringPeak, ok := openAIFreshUpstreamBillingRate(account, time.Date(2026, 7, 13, 17, 59, 0, 0, time.UTC))
	require.True(t, ok)
	require.Equal(t, 0.8, duringPeak)

	afterPeak, ok := openAIFreshUpstreamBillingRate(account, time.Date(2026, 7, 13, 18, 1, 0, 0, time.UTC))
	require.True(t, ok)
	require.Equal(t, 0.4, afterPeak)
}

func TestOpenAIUpstreamCostFactorsSparseProbeIsNeutral(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	accounts := make([]*Account, 0, 10)
	accounts = append(accounts, upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 1, now.Add(-time.Minute), 30*time.Minute))
	for id := int64(2); id <= 10; id++ {
		accounts = append(accounts, &Account{
			ID:       id,
			Platform: PlatformOpenAI,
			Type:     AccountTypeAPIKey,
			Extra: map[string]any{
				UpstreamBillingProbeExtraKey: map[string]any{
					"status":          UpstreamBillingProbeStatusFailed,
					"last_attempt_at": now.UTC().Format(time.RFC3339Nano),
					"next_probe_at":   now.Add(time.Hour).UTC().Format(time.RFC3339Nano),
				},
			},
		})
	}

	factors := openAIUpstreamCostFactors(accounts, now, defaultOpenAIOAuthSchedulingRateMultiplier)
	for id := int64(1); id <= 10; id++ {
		require.Equal(t, openAIUpstreamCostNeutralFactor, factors[id])
	}
}

func TestOpenAIUpstreamCostFactorsCoverageShrinksSparseSignal(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	accounts := []*Account{
		upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute),
		upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.8, now.Add(-time.Minute), 30*time.Minute),
	}
	for id := int64(3); id <= 10; id++ {
		accounts = append(accounts, &Account{ID: id, Platform: PlatformOpenAI, Type: AccountTypeAPIKey})
	}

	factors := openAIUpstreamCostFactors(accounts, now, defaultOpenAIOAuthSchedulingRateMultiplier)
	center := math.Sqrt(0.03 * 0.8)
	require.InDelta(t, 0.5+0.2*(1/(1+0.03/center)-0.5), factors[1], 1e-12)
	require.InDelta(t, 0.5+0.2*(1/(1+0.8/center)-0.5), factors[2], 1e-12)
	require.Equal(t, openAIUpstreamCostNeutralFactor, factors[3])
}

func TestOpenAIUpstreamCostFactorsUseMedianAgainstOutlier(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	accounts := []*Account{
		upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.1, now.Add(-time.Minute), 30*time.Minute),
		upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.2, now.Add(-time.Minute), 30*time.Minute),
		upstreamCostTestAccount(3, UpstreamBillingProbeStatusOK, 100, now.Add(-time.Minute), 30*time.Minute),
	}

	factors := openAIUpstreamCostFactors(accounts, now, defaultOpenAIOAuthSchedulingRateMultiplier)
	require.InDelta(t, 2.0/3.0, factors[1], 1e-12)
	require.InDelta(t, 0.5, factors[2], 1e-12)
	require.InDelta(t, 1/(1+100/0.2), factors[3], 1e-12)
}

func TestOpenAILegacyUpstreamRateOrderRequiresComparableRates(t *testing.T) {
	now := time.Now()
	oneKnown := newOpenAILegacyUpstreamRateOrder([]*Account{
		upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute),
		{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
	}, now, defaultOpenAIOAuthSchedulingRateMultiplier)
	require.False(t, oneKnown.enabled)

	allEqual := newOpenAILegacyUpstreamRateOrder([]*Account{
		upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.3, now.Add(-time.Minute), 30*time.Minute),
		upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.3, now.Add(-time.Minute), 30*time.Minute),
	}, now, defaultOpenAIOAuthSchedulingRateMultiplier)
	require.False(t, allEqual.enabled)

	distinct := newOpenAILegacyUpstreamRateOrder([]*Account{
		upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute),
		upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.8, now.Add(-time.Minute), 30*time.Minute),
		{ID: 3, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
	}, now, defaultOpenAIOAuthSchedulingRateMultiplier)
	require.True(t, distinct.enabled)
	require.Negative(t, distinct.compare(&Account{ID: 1}, &Account{ID: 2}))
	require.Negative(t, distinct.compare(&Account{ID: 2}, &Account{ID: 3}))
}

// 探测资格已放宽到全部 API-key 平台，但调度侧的信任面没有跟着扩大：
// 只有 OpenAI 平台账号的上游自报倍率参与 legacy 低倍率优先排序，
// 否则中转方自报低价即可吸走流量，而实际结算走本地倍率。
// 本用例钉死 newOpenAILegacyUpstreamRateOrder 与 openAIUpstreamCostFactors
// 使用同一道平台门控。
func TestOpenAILegacyUpstreamRateOrderIgnoresNonOpenAIPlatforms(t *testing.T) {
	now := time.Now()
	nonOpenAI := func(id int64, platform string, rate float64) *Account {
		account := upstreamCostTestAccount(id, UpstreamBillingProbeStatusOK, rate, now.Add(-time.Minute), 30*time.Minute)
		account.Platform = platform
		return account
	}
	grokCheap := nonOpenAI(1, PlatformGrok, 0.01)
	anthropicExpensive := nonOpenAI(2, PlatformAnthropic, 0.9)

	order := newOpenAILegacyUpstreamRateOrder([]*Account{grokCheap, anthropicExpensive, nil}, now, defaultOpenAIOAuthSchedulingRateMultiplier)
	require.False(t, order.enabled)
	require.Empty(t, order.rates)
	require.Zero(t, order.compare(grokCheap, anthropicExpensive))

	factors := openAIUpstreamCostFactors([]*Account{grokCheap, anthropicExpensive}, now, defaultOpenAIOAuthSchedulingRateMultiplier)
	require.Equal(t, openAIUpstreamCostNeutralFactor, factors[grokCheap.ID])
	require.Equal(t, openAIUpstreamCostNeutralFactor, factors[anthropicExpensive.ID])

	// 混合候选集里，非 OpenAI 账号既不进 rates 也不影响 OpenAI 账号之间的排序。
	openAICheap := upstreamCostTestAccount(3, UpstreamBillingProbeStatusOK, 0.02, now.Add(-time.Minute), 30*time.Minute)
	openAIExpensive := upstreamCostTestAccount(4, UpstreamBillingProbeStatusOK, 0.12, now.Add(-time.Minute), 30*time.Minute)
	mixed := newOpenAILegacyUpstreamRateOrder(
		[]*Account{grokCheap, openAICheap, anthropicExpensive, openAIExpensive},
		now, defaultOpenAIOAuthSchedulingRateMultiplier,
	)
	require.True(t, mixed.enabled)
	require.Len(t, mixed.rates, 2)
	require.NotContains(t, mixed.rates, grokCheap.ID)
	require.NotContains(t, mixed.rates, anthropicExpensive.ID)
	require.Negative(t, mixed.compare(openAICheap, openAIExpensive))
	// 自报 0.01 的 grok 账号没有已知倍率，排在有倍率的 OpenAI 账号之后。
	require.Positive(t, mixed.compare(grokCheap, openAIExpensive))
}

func TestOpenAISchedulingRatePlacesOAuthAtConfiguredReference(t *testing.T) {
	now := time.Now()
	cheap := upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.02, now.Add(-time.Minute), 30*time.Minute)
	oauth := upstreamCostTestOAuthAccount(2)
	expensive := upstreamCostTestAccount(3, UpstreamBillingProbeStatusOK, 0.12, now.Add(-time.Minute), 30*time.Minute)

	order := newOpenAILegacyUpstreamRateOrder([]*Account{cheap, oauth, expensive}, now, 0.05)
	require.True(t, order.enabled)
	require.Negative(t, order.compare(cheap, oauth))
	require.Negative(t, order.compare(oauth, expensive))

	factors := openAIUpstreamCostFactors([]*Account{cheap, oauth, expensive}, now, 0.05)
	require.Greater(t, factors[cheap.ID], factors[oauth.ID])
	require.Greater(t, factors[oauth.ID], factors[expensive.ID])
}

func TestOpenAIGatewayServiceLegacyLowRatePriorityUsesConfiguredOAuthReference(t *testing.T) {
	resetOpenAIAdvancedSchedulerSettingCacheForTest()
	defer resetOpenAIAdvancedSchedulerSettingCacheForTest()

	now := time.Now()
	cheap := upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.02, now.Add(-time.Minute), 30*time.Minute)
	oauth := upstreamCostTestOAuthAccount(2)
	expensive := upstreamCostTestAccount(3, UpstreamBillingProbeStatusOK, 0.12, now.Add(-time.Minute), 30*time.Minute)
	for _, account := range []*Account{cheap, oauth, expensive} {
		account.Status = StatusActive
		account.Schedulable = true
		account.Concurrency = 1
	}
	cheap.Priority, oauth.Priority, expensive.Priority = 20, 10, 0

	settings := &openAIAdvancedSchedulerSettingRepoStub{values: map[string]string{
		openAIAdvancedSchedulerSettingKey:              "false",
		SettingKeyOpenAILowUpstreamRatePriorityEnabled: "true",
		SettingKeyOpenAIOAuthSchedulingRateMultiplier:  "0.05",
	}}
	cfg := &config.Config{}
	svc := &OpenAIGatewayService{
		accountRepo:      schedulerTestOpenAIAccountRepo{accounts: []Account{*cheap, *oauth, *expensive}},
		cache:            &schedulerTestGatewayCache{},
		cfg:              cfg,
		rateLimitService: &RateLimitService{settingService: NewSettingService(settings, cfg)},
	}
	groupID := int64(1)

	first, _, err := svc.SelectAccountWithScheduler(context.Background(), &groupID, "", "", "gpt-test", nil, OpenAIUpstreamTransportAny, false)
	require.NoError(t, err)
	require.Equal(t, cheap.ID, first.Account.ID)

	second, _, err := svc.SelectAccountWithScheduler(context.Background(), &groupID, "", "", "gpt-test", map[int64]struct{}{cheap.ID: {}}, OpenAIUpstreamTransportAny, false)
	require.NoError(t, err)
	require.Equal(t, oauth.ID, second.Account.ID)
}

func TestOpenAIModelsSelectionIgnoresTokenCostSignal(t *testing.T) {
	resetOpenAIAdvancedSchedulerSettingCacheForTest()
	defer resetOpenAIAdvancedSchedulerSettingCacheForTest()

	now := time.Now()
	cheap := upstreamCostTestAccount(51, UpstreamBillingProbeStatusOK, 0.02, now.Add(-time.Minute), 30*time.Minute)
	expensive := upstreamCostTestAccount(52, UpstreamBillingProbeStatusOK, 0.8, now.Add(-time.Minute), 30*time.Minute)
	for _, account := range []*Account{cheap, expensive} {
		account.Status = StatusActive
		account.Schedulable = true
		account.Concurrency = 1
	}
	cheap.Priority = 10
	expensive.Priority = 0
	settings := &openAIAdvancedSchedulerSettingRepoStub{values: map[string]string{
		SettingKeyOpenAILowUpstreamRatePriorityEnabled: "true",
	}}
	cfg := &config.Config{}
	svc := &OpenAIGatewayService{
		accountRepo:      schedulerTestOpenAIAccountRepo{accounts: []Account{*cheap, *expensive}},
		cfg:              cfg,
		rateLimitService: &RateLimitService{settingService: NewSettingService(settings, cfg)},
	}

	account, err := svc.SelectAccountForModelWithExclusions(context.Background(), nil, "", "", nil)
	require.NoError(t, err)
	require.Equal(t, expensive.ID, account.ID)
}

func TestOpenAIGatewayServiceLegacyLowRatePriorityIsIndependentFromAdvancedScheduler(t *testing.T) {
	now := time.Now()
	cheap := upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute)
	cheap.Status, cheap.Schedulable, cheap.Concurrency, cheap.Priority = StatusActive, true, 1, 10
	expensive := upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.8, now.Add(-time.Minute), 30*time.Minute)
	expensive.Status, expensive.Schedulable, expensive.Concurrency, expensive.Priority = StatusActive, true, 1, 0
	accounts := []Account{*cheap, *expensive}
	groupID := int64(1)

	tests := []struct {
		name      string
		enabled   bool
		loadBatch bool
		loadErr   error
		wantID    int64
	}{
		{name: "switch off keeps priority first", loadBatch: true, wantID: 2},
		{name: "load batch", enabled: true, loadBatch: true, wantID: 1},
		{name: "load batch disabled", enabled: true, wantID: 1},
		{name: "load lookup failure", enabled: true, loadBatch: true, loadErr: errors.New("load unavailable"), wantID: 1},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			resetOpenAIAdvancedSchedulerSettingCacheForTest()
			settings := &openAIAdvancedSchedulerSettingRepoStub{values: map[string]string{
				openAIAdvancedSchedulerSettingKey:              "false",
				SettingKeyOpenAILowUpstreamRatePriorityEnabled: strconv.FormatBool(tt.enabled),
			}}
			cfg := &config.Config{}
			cfg.Gateway.Scheduling.LoadBatchEnabled = tt.loadBatch
			svc := &OpenAIGatewayService{
				accountRepo:      schedulerTestOpenAIAccountRepo{accounts: accounts},
				cache:            &schedulerTestGatewayCache{},
				cfg:              cfg,
				rateLimitService: &RateLimitService{settingService: NewSettingService(settings, cfg)},
				concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{
					loadBatchErr: tt.loadErr,
					loadMap: map[int64]*AccountLoadInfo{
						1: {AccountID: 1, LoadRate: 90},
						2: {AccountID: 2, LoadRate: 10},
					},
				}),
			}

			selection, _, err := svc.SelectAccountWithScheduler(context.Background(), &groupID, "", "", "gpt-test", nil, OpenAIUpstreamTransportAny, false)
			require.NoError(t, err)
			require.Equal(t, tt.wantID, selection.Account.ID)
			if selection.ReleaseFunc != nil {
				selection.ReleaseFunc()
			}
		})
	}
}

func TestOpenAIGatewayServiceAdvancedSchedulerIgnoresLegacyLowRateSwitch(t *testing.T) {
	resetOpenAIAdvancedSchedulerSettingCacheForTest()
	defer resetOpenAIAdvancedSchedulerSettingCacheForTest()
	now := time.Now()
	cheap := upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute)
	cheap.Status, cheap.Schedulable, cheap.Concurrency, cheap.Priority = StatusActive, true, 1, 10
	expensive := upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.8, now.Add(-time.Minute), 30*time.Minute)
	expensive.Status, expensive.Schedulable, expensive.Concurrency, expensive.Priority = StatusActive, true, 1, 0
	settings := &openAIAdvancedSchedulerSettingRepoStub{values: map[string]string{
		openAIAdvancedSchedulerSettingKey:              "true",
		SettingKeyOpenAILowUpstreamRatePriorityEnabled: "true",
	}}
	cfg := &config.Config{}
	cfg.Gateway.OpenAIWS.LBTopK = 1
	cfg.Gateway.OpenAIWS.SchedulerScoreWeights.Priority = 1
	svc := &OpenAIGatewayService{
		accountRepo:        schedulerTestOpenAIAccountRepo{accounts: []Account{*cheap, *expensive}},
		cache:              &schedulerTestGatewayCache{},
		cfg:                cfg,
		rateLimitService:   &RateLimitService{settingService: NewSettingService(settings, cfg)},
		concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{}),
	}
	groupID := int64(1)

	selection, _, err := svc.SelectAccountWithScheduler(context.Background(), &groupID, "", "", "gpt-test", nil, OpenAIUpstreamTransportAny, false)
	require.NoError(t, err)
	require.Equal(t, int64(2), selection.Account.ID)
	if selection.ReleaseFunc != nil {
		selection.ReleaseFunc()
	}
}

func TestOpenAIGatewayServiceLegacyLowRatePrioritySkipsCooledDownAccount(t *testing.T) {
	resetOpenAIAdvancedSchedulerSettingCacheForTest()
	defer resetOpenAIAdvancedSchedulerSettingCacheForTest()

	now := time.Now()
	cheap := upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute)
	cheap.Status, cheap.Schedulable, cheap.Concurrency, cheap.Priority = StatusActive, true, 1, 10
	cooldownUntil := now.Add(time.Minute)
	cheap.TempUnschedulableUntil = &cooldownUntil
	expensive := upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.8, now.Add(-time.Minute), 30*time.Minute)
	expensive.Status, expensive.Schedulable, expensive.Concurrency, expensive.Priority = StatusActive, true, 1, 0
	settings := &openAIAdvancedSchedulerSettingRepoStub{values: map[string]string{
		openAIAdvancedSchedulerSettingKey:              "false",
		SettingKeyOpenAILowUpstreamRatePriorityEnabled: "true",
	}}
	cfg := &config.Config{}
	cfg.Gateway.Scheduling.LoadBatchEnabled = true
	svc := &OpenAIGatewayService{
		accountRepo:      schedulerTestOpenAIAccountRepo{accounts: []Account{*cheap, *expensive}},
		cache:            &schedulerTestGatewayCache{},
		cfg:              cfg,
		rateLimitService: &RateLimitService{settingService: NewSettingService(settings, cfg)},
		concurrencyService: NewConcurrencyService(schedulerTestConcurrencyCache{loadMap: map[int64]*AccountLoadInfo{
			1: {AccountID: 1},
			2: {AccountID: 2},
		}}),
	}
	groupID := int64(1)

	selection, _, err := svc.SelectAccountWithScheduler(context.Background(), &groupID, "", "", "gpt-test", nil, OpenAIUpstreamTransportAny, false)
	require.NoError(t, err)
	require.Equal(t, int64(2), selection.Account.ID)
	if selection.ReleaseFunc != nil {
		selection.ReleaseFunc()
	}
}

func TestOpenAIFreshUpstreamBillingRateUsesFreshCachedSuccessOnly(t *testing.T) {
	now := time.Date(2026, 7, 13, 12, 0, 0, 0, time.UTC)
	tests := []struct {
		name    string
		account *Account
		wantOK  bool
	}{
		{name: "fresh", account: upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.3, now.Add(-time.Minute), 30*time.Minute), wantOK: true},
		{name: "zero rate", account: upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0, now.Add(-time.Minute), 30*time.Minute), wantOK: true},
		{name: "transient failure with fresh cache", account: upstreamCostTestAccount(3, UpstreamBillingProbeStatusFailed, 0.3, now.Add(-time.Minute), 30*time.Minute), wantOK: true},
		{name: "stale", account: upstreamCostTestAccount(4, UpstreamBillingProbeStatusOK, 0.3, now.Add(-61*time.Minute), 30*time.Minute)},
		{name: "future", account: upstreamCostTestAccount(5, UpstreamBillingProbeStatusOK, 0.3, now.Add(time.Minute), 30*time.Minute)},
		{name: "unsupported", account: upstreamCostTestAccount(6, UpstreamBillingProbeStatusUnsupported, 0.3, now.Add(-time.Minute), 30*time.Minute)},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, ok := openAIFreshUpstreamBillingRate(tt.account, now)
			require.Equal(t, tt.wantOK, ok)
		})
	}
}

func TestBuildOpenAISelectionOrderIncludesOverflowOnlyForCostScheduling(t *testing.T) {
	scheduler := &defaultOpenAIAccountScheduler{}
	candidates := []openAIAccountCandidateScore{
		{account: &Account{ID: 1}, loadInfo: &AccountLoadInfo{}, score: 3},
		{account: &Account{ID: 2}, loadInfo: &AccountLoadInfo{}, score: 2},
		{account: &Account{ID: 3}, loadInfo: &AccountLoadInfo{}, score: 1},
	}

	legacy := scheduler.buildOpenAISelectionOrder(OpenAIAccountScheduleRequest{}, openAIAccountLoadPlan{
		candidates: candidates,
		topK:       1,
	})
	require.Len(t, legacy, 1)

	costAware := scheduler.buildOpenAISelectionOrder(OpenAIAccountScheduleRequest{}, openAIAccountLoadPlan{
		candidates:              candidates,
		topK:                    1,
		includeOverflowFallback: true,
	})
	require.Equal(t, []int64{1, 2, 3}, []int64{
		costAware[0].account.ID,
		costAware[1].account.ID,
		costAware[2].account.ID,
	})
}

func TestBuildOpenAIAccountLoadPlanUsesCostOnlyForTokenScope(t *testing.T) {
	resetOpenAIAdvancedSchedulerSettingCacheForTest()
	defer resetOpenAIAdvancedSchedulerSettingCacheForTest()

	now := time.Now()
	accounts := []*Account{
		upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute),
		upstreamCostTestOAuthAccount(2),
		upstreamCostTestAccount(3, UpstreamBillingProbeStatusOK, 0.8, now.Add(-time.Minute), 30*time.Minute),
	}
	cfg := &config.Config{}
	cfg.Gateway.OpenAIWS.LBTopK = 1
	cfg.Gateway.OpenAIWS.SchedulerScoreWeights.UpstreamCost = 1.5
	settings := &openAIAdvancedSchedulerSettingRepoStub{values: map[string]string{
		SettingKeyOpenAIOAuthSchedulingRateMultiplier: "0.05",
	}}
	scheduler := &defaultOpenAIAccountScheduler{service: &OpenAIGatewayService{
		cfg:              cfg,
		rateLimitService: &RateLimitService{settingService: NewSettingService(settings, cfg)},
	}}
	loadMap := map[int64]*AccountLoadInfo{
		1: {AccountID: 1},
		2: {AccountID: 2},
		3: {AccountID: 3},
	}

	tokenPlan := scheduler.buildOpenAIAccountLoadPlan(context.Background(), OpenAIAccountScheduleRequest{UseUpstreamTokenCost: true}, accounts, loadMap)
	require.Greater(t, tokenPlan.candidates[0].score, tokenPlan.candidates[1].score)
	require.Greater(t, tokenPlan.candidates[1].score, tokenPlan.candidates[2].score)
	require.True(t, tokenPlan.includeOverflowFallback)

	otherPlan := scheduler.buildOpenAIAccountLoadPlan(context.Background(), OpenAIAccountScheduleRequest{}, accounts, loadMap)
	require.Equal(t, otherPlan.candidates[0].score, otherPlan.candidates[1].score)
	require.Equal(t, otherPlan.candidates[1].score, otherPlan.candidates[2].score)
	require.False(t, otherPlan.includeOverflowFallback)
}

func TestBuildOpenAIAccountSchedulerScoreSnapshotUpstreamCostIsExactNoOpWithoutSignal(t *testing.T) {
	accounts := []*Account{
		{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
		{ID: 2, Platform: PlatformOpenAI, Type: AccountTypeAPIKey},
	}
	loadMap := map[int64]*AccountLoadInfo{
		1: {AccountID: 1, LoadRate: 20},
		2: {AccountID: 2, LoadRate: 80},
	}
	weights := GatewayOpenAIWSSchedulerScoreWeightsView{Priority: 1, Load: 1, Queue: 0.7, ErrorRate: 0.8, TTFT: 0.5}
	baseline := buildOpenAIAccountSchedulerScoreSnapshot(accounts, loadMap, weights, false, defaultOpenAIOAuthSchedulingRateMultiplier)
	weights.UpstreamCost = 1.5
	withCost := buildOpenAIAccountSchedulerScoreSnapshot(accounts, loadMap, weights, false, defaultOpenAIOAuthSchedulingRateMultiplier)

	require.Equal(t, baseline, withCost)
}

func TestBuildOpenAIAccountSchedulerScoreSnapshotUsesUpstreamCostSignal(t *testing.T) {
	now := time.Now()
	accounts := []*Account{
		upstreamCostTestAccount(1, UpstreamBillingProbeStatusOK, 0.03, now.Add(-time.Minute), 30*time.Minute),
		upstreamCostTestAccount(2, UpstreamBillingProbeStatusOK, 0.8, now.Add(-time.Minute), 30*time.Minute),
	}
	weights := GatewayOpenAIWSSchedulerScoreWeightsView{UpstreamCost: 1.5}
	scores := buildOpenAIAccountSchedulerScoreSnapshot(accounts, nil, weights, false, defaultOpenAIOAuthSchedulingRateMultiplier)

	require.Greater(t, scores[1].BaseScore, scores[2].BaseScore)
}
