//go:build unit

package service

import (
	"context"
	"testing"
	"time"

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

// --- repo / fetcher 装配 ---

// quotaModeRepoStub 记录 RunCheck 落库行为（历史行 + MarkChecked）。
type quotaModeRepoStub struct {
	ChannelMonitorRepository
	monitor   *ChannelMonitor
	history   []*ChannelMonitorHistoryRow
	markedIDs []int64
	updated   []*ChannelMonitor
}

func (r *quotaModeRepoStub) GetByID(_ context.Context, id int64) (*ChannelMonitor, error) {
	if r.monitor == nil || r.monitor.ID != id {
		return nil, ErrChannelMonitorNotFound
	}
	clone := *r.monitor
	return &clone, nil
}

func (r *quotaModeRepoStub) InsertHistoryBatch(_ context.Context, rows []*ChannelMonitorHistoryRow) error {
	r.history = append(r.history, rows...)
	return nil
}

func (r *quotaModeRepoStub) MarkChecked(_ context.Context, id int64, _ time.Time) error {
	r.markedIDs = append(r.markedIDs, id)
	return nil
}

func (r *quotaModeRepoStub) Update(_ context.Context, m *ChannelMonitor) error {
	clone := *m
	r.updated = append(r.updated, &clone)
	return nil
}

// newQuotaModeService 构造启用 V1 探活的 service（复用 retirement/duplicate 测试的 stub）。
func newQuotaModeService(repo *quotaModeRepoStub) *ChannelMonitorService {
	svc := NewChannelMonitorService(repo, &duplicateChannelMonitorEncryptor{})
	svc.SetRuntimeReader(channelMonitorRuntimeStub{rt: ChannelMonitorRuntime{
		Enabled: true,
		Mode:    ChannelMonitorModeV1,
	}})
	return svc
}

func newQuotaModeFetcher(accounts map[int64]*Account, usage *stubMonitorUsageSource) *ChannelMonitorQuotaFetcher {
	if accounts == nil {
		accounts = make(map[int64]*Account)
	}
	if usage == nil {
		usage = &stubMonitorUsageSource{}
	}
	return &ChannelMonitorQuotaFetcher{
		usage:    usage,
		accounts: &stubMonitorAccountSource{accounts: accounts},
		cache:    make(map[int64]monitorQuotaCacheEntry),
	}
}

// --- RunCheck 分派 ---

func TestRunCheck_QuotaModeProducesSingleQuotaResult(t *testing.T) {
	repo := &quotaModeRepoStub{monitor: &ChannelMonitor{
		ID:              1,
		Name:            "kimi-quota",
		Provider:        MonitorProviderKimi,
		APIMode:         MonitorAPIModeChatCompletions,
		PrimaryModel:    "quota",
		Enabled:         true,
		IntervalSeconds: 60,
		CheckMode:       MonitorCheckModeQuota,
		AccountID:       int64Ptr(9),
	}}
	svc := newQuotaModeService(repo)
	fetcher := newQuotaModeFetcher(map[int64]*Account{
		9: {ID: 9, Platform: domain.PlatformKimi, Credentials: map[string]any{"account_mode": AccountModeCoding}},
	}, nil)
	fetcher.cnQuota = &stubMonitorCNQuotaSource{result: &CNProviderQuotaProbeResult{
		Success:         true,
		CredentialValid: true,
		Tiers:           []CNQuotaTier{{Window: "5h", UsedPercent: 30}},
	}}
	svc.SetQuotaFetcher(fetcher)

	results, err := svc.RunCheck(context.Background(), 1)
	require.NoError(t, err)
	require.Len(t, results, 1)

	res := results[0]
	require.Equal(t, "quota", res.Model)
	require.Equal(t, MonitorStatusOperational, res.Status)
	require.Nil(t, res.LatencyMs)
	require.Nil(t, res.PingLatencyMs)
	require.NotNil(t, res.Quota)
	require.True(t, res.Quota.Success)
	require.Equal(t, "cn_quota", res.Quota.Source)

	// 历史行携带配额快照，并推进 last_checked_at。
	require.Len(t, repo.history, 1)
	require.Equal(t, "quota", repo.history[0].Model)
	require.NotNil(t, repo.history[0].Quota)
	require.Equal(t, []int64{1}, repo.markedIDs)
}

func TestRunCheck_QuotaModeUnlinkedAccountDegrades(t *testing.T) {
	repo := &quotaModeRepoStub{monitor: &ChannelMonitor{
		ID:              2,
		Provider:        MonitorProviderDeepseek,
		APIMode:         MonitorAPIModeChatCompletions,
		Endpoint:        "",
		PrimaryModel:    "quota",
		Enabled:         true,
		IntervalSeconds: 60,
		CheckMode:       MonitorCheckModeQuota,
		AccountID:       nil, // FK ON DELETE SET NULL 后的形态
	}}
	svc := newQuotaModeService(repo)
	svc.SetQuotaFetcher(newQuotaModeFetcher(nil, nil))

	results, err := svc.RunCheck(context.Background(), 2)
	require.NoError(t, err)
	require.Len(t, results, 1)
	require.Equal(t, MonitorStatusDegraded, results[0].Status)
	require.Contains(t, results[0].Message, "linked account not found")
	require.False(t, results[0].Quota.Success)
}

func TestRunCheck_QuotaModeNilFetcherFailsClosed(t *testing.T) {
	repo := &quotaModeRepoStub{monitor: &ChannelMonitor{
		ID:              3,
		Provider:        MonitorProviderZhipu,
		APIMode:         MonitorAPIModeChatCompletions,
		PrimaryModel:    "quota",
		Enabled:         true,
		IntervalSeconds: 60,
		CheckMode:       MonitorCheckModeQuota,
		AccountID:       int64Ptr(5),
	}}
	svc := newQuotaModeService(repo) // 不注入 fetcher

	results, err := svc.RunCheck(context.Background(), 3)
	require.NoError(t, err)
	require.Len(t, results, 1)
	require.Equal(t, MonitorStatusError, results[0].Status)
	require.Contains(t, results[0].Message, "not configured")
}

func TestRunCheck_QuotaProbeAttachesSnapshotToPrimaryRowOnly(t *testing.T) {
	h := &openAICaptureHandler{}
	endpoint := setupFakeOpenAI(t, h)
	repo := &quotaModeRepoStub{monitor: &ChannelMonitor{
		ID:              4,
		Provider:        MonitorProviderOpenAI,
		APIMode:         MonitorAPIModeChatCompletions,
		Endpoint:        endpoint,
		APIKey:          "OLD:sk-openai",
		PrimaryModel:    "gpt-test",
		ExtraModels:     []string{"gpt-extra"},
		Enabled:         true,
		IntervalSeconds: 60,
		CheckMode:       MonitorCheckModeQuotaProbe,
		AccountID:       int64Ptr(12),
	}}
	svc := newQuotaModeService(repo)
	usage := &stubMonitorUsageSource{usage: &UsageInfo{
		FiveHour: &UsageProgress{Utilization: 20},
	}}
	svc.SetQuotaFetcher(newQuotaModeFetcher(map[int64]*Account{
		12: {ID: 12, Platform: domain.PlatformOpenAI},
	}, usage))

	results, err := svc.RunCheck(context.Background(), 4)
	require.NoError(t, err)
	require.Len(t, results, 2)

	// 探活状态为准，配额只挂主模型行。
	require.Equal(t, MonitorStatusOperational, results[0].Status)
	require.NotNil(t, results[0].Quota)
	require.True(t, results[0].Quota.Success)
	require.Equal(t, "usage", results[0].Quota.Source)
	require.Nil(t, results[1].Quota, "extra model rows must not carry quota")

	// 历史落库时同样只有主模型行带快照。
	require.Len(t, repo.history, 2)
	require.NotNil(t, repo.history[0].Quota)
	require.Equal(t, "gpt-test", repo.history[0].Model)
	require.Nil(t, repo.history[1].Quota)
}

func TestRunCheck_QuotaProbeQuotaFailureKeepsProbeStatus(t *testing.T) {
	h := &openAICaptureHandler{}
	endpoint := setupFakeOpenAI(t, h)
	repo := &quotaModeRepoStub{monitor: &ChannelMonitor{
		ID:              5,
		Provider:        MonitorProviderOpenAI,
		APIMode:         MonitorAPIModeChatCompletions,
		Endpoint:        endpoint,
		APIKey:          "OLD:sk-openai",
		PrimaryModel:    "gpt-test",
		Enabled:         true,
		IntervalSeconds: 60,
		CheckMode:       MonitorCheckModeQuotaProbe,
		AccountID:       nil, // 配额侧失效
	}}
	svc := newQuotaModeService(repo)
	svc.SetQuotaFetcher(newQuotaModeFetcher(nil, nil))

	results, err := svc.RunCheck(context.Background(), 5)
	require.NoError(t, err)
	require.Len(t, results, 1)
	require.Equal(t, MonitorStatusOperational, results[0].Status, "quota failure must not flip probe status")
	require.False(t, results[0].Quota.Success)
}

// --- attachQuotaSnapshot 细节 ---

func TestAttachQuotaSnapshot_NoteOnlyWhenProbeMessageEmpty(t *testing.T) {
	results := []*CheckResult{
		{Model: "primary", Status: MonitorStatusOperational, Message: "challenge passed"},
		{Model: "extra"},
	}
	failed := &domain.MonitorQuotaSnapshot{Success: false, Error: "boom"}

	attachQuotaSnapshot(results, failed)

	require.Equal(t, "challenge passed", results[0].Message, "existing probe message wins")
	require.Equal(t, failed, results[0].Quota)
	require.Nil(t, results[1].Quota)

	quiet := []*CheckResult{{Model: "primary", Status: MonitorStatusOperational}}
	attachQuotaSnapshot(quiet, failed)
	require.Contains(t, quiet[0].Message, "quota fetch failed: boom")

	attachQuotaSnapshot(nil, failed)  // 空结果不 panic
	attachQuotaSnapshot(results, nil) // 空快照不动结果
}

// --- 校验矩阵 ---

func TestValidateCreateParams_CheckModeMatrix(t *testing.T) {
	accountID := int64(9)

	cases := []struct {
		name    string
		params  ChannelMonitorCreateParams
		wantErr error
	}{
		{
			name: "probe requires endpoint",
			params: ChannelMonitorCreateParams{
				Provider: MonitorProviderOpenAI, CheckMode: MonitorCheckModeProbe,
				APIKey: "sk", IntervalSeconds: 60, PrimaryModel: "gpt-5",
			},
			wantErr: ErrChannelMonitorInvalidEndpoint,
		},
		{
			name: "probe requires api key",
			params: ChannelMonitorCreateParams{
				Provider: MonitorProviderOpenAI, CheckMode: MonitorCheckModeProbe,
				Endpoint: "https://api.openai.com", IntervalSeconds: 60, PrimaryModel: "gpt-5",
			},
			wantErr: ErrChannelMonitorMissingAPIKey,
		},
		{
			name: "quota drops endpoint and api key requirements",
			params: ChannelMonitorCreateParams{
				Provider: MonitorProviderAntigravity, CheckMode: MonitorCheckModeQuota,
				IntervalSeconds: 60, AccountID: &accountID,
			},
			wantErr: nil, // primary_model 默认 "quota"
		},
		{
			name: "quota requires account",
			params: ChannelMonitorCreateParams{
				Provider: MonitorProviderKimi, CheckMode: MonitorCheckModeQuota,
				IntervalSeconds: 60, PrimaryModel: "quota",
			},
			wantErr: ErrChannelMonitorAccountRequired,
		},
		{
			name: "quota_probe requires endpoint and api key too",
			params: ChannelMonitorCreateParams{
				Provider: MonitorProviderKimi, CheckMode: MonitorCheckModeQuotaProbe,
				IntervalSeconds: 60, AccountID: &accountID, PrimaryModel: "kimi-k2",
			},
			wantErr: ErrChannelMonitorInvalidEndpoint,
		},
		{
			name: "antigravity probe unsupported",
			params: ChannelMonitorCreateParams{
				Provider: MonitorProviderAntigravity, CheckMode: MonitorCheckModeProbe,
				Endpoint: "https://example.com", APIKey: "k",
				IntervalSeconds: 60, AccountID: &accountID, PrimaryModel: "gemini-3-pro",
			},
			wantErr: ErrChannelMonitorInvalidCheckMode,
		},
		{
			name: "antigravity quota_probe unsupported",
			params: ChannelMonitorCreateParams{
				Provider: MonitorProviderAntigravity, CheckMode: MonitorCheckModeQuotaProbe,
				Endpoint: "https://example.com", APIKey: "k",
				IntervalSeconds: 60, AccountID: &accountID, PrimaryModel: "gemini-3-pro",
			},
			wantErr: ErrChannelMonitorInvalidCheckMode,
		},
		{
			name: "unknown mode rejected",
			params: ChannelMonitorCreateParams{
				Provider: MonitorProviderOpenAI, CheckMode: "auto",
				Endpoint: "https://api.openai.com", APIKey: "sk",
				IntervalSeconds: 60, PrimaryModel: "gpt-5",
			},
			wantErr: ErrChannelMonitorInvalidCheckMode,
		},
		{
			// quota_probe 仍要打真实探活请求：空模型必须报错，不再用 "quota" 占位。
			name: "quota_probe requires primary model",
			params: ChannelMonitorCreateParams{
				Provider: MonitorProviderKimi, CheckMode: MonitorCheckModeQuotaProbe,
				Endpoint: "https://api.kimi.com", APIKey: "sk",
				IntervalSeconds: 60, AccountID: &accountID,
			},
			wantErr: ErrChannelMonitorMissingPrimaryModel,
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := validateCreateParams(tc.params)
			if tc.wantErr == nil {
				require.NoError(t, err)
			} else {
				require.ErrorIs(t, err, tc.wantErr)
			}
		})
	}
}

func TestNormalizeMonitorPrimaryModel_QuotaDefault(t *testing.T) {
	require.Equal(t, "quota", normalizeMonitorPrimaryModel(MonitorProviderKimi, MonitorCheckModeQuota, ""))
	require.Equal(t, "quota", normalizeMonitorPrimaryModel(MonitorProviderAntigravity, MonitorCheckModeQuota, "  "))
	// quota_probe 仍要打真实探活请求：空模型返回 ""（由上层报 MissingPrimaryModel），
	// 不再用 "quota" 占位打 model="quota" 的请求。
	require.Equal(t, "", normalizeMonitorPrimaryModel(MonitorProviderKimi, MonitorCheckModeQuotaProbe, ""))
	// grok 分支在纯 quota 占位之后：grok+quota 占位 "quota"，
	// grok 探活（probe/quota_probe）默认轻量测活模型。
	require.Equal(t, "quota", normalizeMonitorPrimaryModel(MonitorProviderGrok, MonitorCheckModeQuota, ""))
	require.Equal(t, MonitorDefaultGrokModel, normalizeMonitorPrimaryModel(MonitorProviderGrok, MonitorCheckModeProbe, ""))
	require.Equal(t, MonitorDefaultGrokModel, normalizeMonitorPrimaryModel(MonitorProviderGrok, MonitorCheckModeQuotaProbe, ""))
	// 探活模式沿用原语义：其余必填（空串报错在 validateCreateParams）。
	require.Equal(t, "kimi-k2", normalizeMonitorPrimaryModel(MonitorProviderKimi, MonitorCheckModeQuotaProbe, "kimi-k2"))
}

func TestProviderProbeCapabilityMatrix(t *testing.T) {
	require.False(t, providerSupportsProbe(MonitorProviderAntigravity))
	for _, p := range []string{
		MonitorProviderOpenAI, MonitorProviderAnthropic, MonitorProviderGemini,
		MonitorProviderGrok, MonitorProviderKimi, MonitorProviderZhipu, MonitorProviderDeepseek,
	} {
		require.True(t, providerSupportsProbe(p), p)
	}
	for _, p := range []string{
		MonitorProviderOpenAI, MonitorProviderAnthropic, MonitorProviderGemini,
		MonitorProviderGrok, MonitorProviderAntigravity,
		MonitorProviderKimi, MonitorProviderZhipu, MonitorProviderDeepseek,
	} {
		require.NoError(t, validateProvider(p), p)
	}
}

// --- 关联账号校验 ---

func TestValidateLinkedAccount_Matrix(t *testing.T) {
	svc := NewChannelMonitorService(nil, nil)
	fetcher := newQuotaModeFetcher(map[int64]*Account{
		1: {ID: 1, Platform: domain.PlatformKimi},
	}, nil)
	svc.SetQuotaFetcher(fetcher)

	require.NoError(t, svc.validateLinkedAccount(context.Background(), MonitorProviderKimi, nil))
	require.NoError(t, svc.validateLinkedAccount(context.Background(), MonitorProviderKimi, int64Ptr(0)))
	require.NoError(t, svc.validateLinkedAccount(context.Background(), MonitorProviderKimi, int64Ptr(1)))
	require.ErrorIs(t, svc.validateLinkedAccount(context.Background(), MonitorProviderZhipu, int64Ptr(1)), ErrChannelMonitorProviderIncompatible)
	require.ErrorIs(t, svc.validateLinkedAccount(context.Background(), MonitorProviderKimi, int64Ptr(404)), ErrChannelMonitorAccountRequired)

	noFetcher := NewChannelMonitorService(nil, nil)
	require.ErrorIs(t, noFetcher.validateLinkedAccount(context.Background(), MonitorProviderKimi, int64Ptr(1)), ErrChannelMonitorAccountRequired)
}

func TestRevalidateLinkedAccount_QuotaErrorsProbeUnbinds(t *testing.T) {
	fetcher := newQuotaModeFetcher(nil, nil) // 账号一律加载失败
	svc := NewChannelMonitorService(nil, nil)
	svc.SetQuotaFetcher(fetcher)

	quota := &ChannelMonitor{Provider: MonitorProviderKimi, CheckMode: MonitorCheckModeQuota, AccountID: int64Ptr(9)}
	require.ErrorIs(t, svc.revalidateLinkedAccount(context.Background(), quota), ErrChannelMonitorAccountRequired)
	require.NotNil(t, quota.AccountID)

	probe := &ChannelMonitor{Provider: MonitorProviderKimi, CheckMode: MonitorCheckModeProbe, AccountID: int64Ptr(9)}
	require.NoError(t, svc.revalidateLinkedAccount(context.Background(), probe))
	require.Nil(t, probe.AccountID, "probe mode should silently unbind stale account")

	quotaNoAccount := &ChannelMonitor{Provider: MonitorProviderKimi, CheckMode: MonitorCheckModeQuota}
	require.ErrorIs(t, svc.revalidateLinkedAccount(context.Background(), quotaNoAccount), ErrChannelMonitorAccountRequired)
}

func TestRevalidateLinkedAccount_PlatformMismatch(t *testing.T) {
	svc := NewChannelMonitorService(nil, nil)
	svc.SetQuotaFetcher(newQuotaModeFetcher(map[int64]*Account{
		2: {ID: 2, Platform: domain.PlatformDeepseek},
	}, nil))

	quota := &ChannelMonitor{Provider: MonitorProviderKimi, CheckMode: MonitorCheckModeQuota, AccountID: int64Ptr(2)}
	require.ErrorIs(t, svc.revalidateLinkedAccount(context.Background(), quota), ErrChannelMonitorProviderIncompatible)

	probe := &ChannelMonitor{Provider: MonitorProviderKimi, CheckMode: MonitorCheckModeProbe, AccountID: int64Ptr(2)}
	require.NoError(t, svc.revalidateLinkedAccount(context.Background(), probe))
	require.Nil(t, probe.AccountID)
}

// 能力矩阵：与 fetchUncached 路由一一对应，创建期拦截注定运行期永久 error 的组合。
func TestMonitorAccountQuotaCapability_Matrix(t *testing.T) {
	cases := []struct {
		name    string
		account *Account
		wantErr error
	}{
		{
			name:    "deepseek coding has no quota endpoint",
			account: &Account{ID: 1, Platform: domain.PlatformDeepseek, Credentials: map[string]any{"account_mode": AccountModeCoding}},
			wantErr: ErrChannelMonitorAccountNotSupportable,
		},
		{
			// 自定义域名 kimi coding：GetCodingPlanProvider 识别不到 → 无额度端点。
			name: "custom-domain kimi coding unsupported",
			account: &Account{ID: 2, Platform: domain.PlatformKimi, Type: AccountTypeAPIKey,
				Credentials: map[string]any{"account_mode": AccountModeCoding, "base_url": "https://cw.example.com"}},
			wantErr: ErrChannelMonitorAccountNotSupportable,
		},
		{
			name:    "kimi coding default endpoint ok",
			account: &Account{ID: 3, Platform: domain.PlatformKimi, Credentials: map[string]any{"account_mode": AccountModeCoding}},
		},
		{
			name:    "zhipu coding default endpoint ok",
			account: &Account{ID: 4, Platform: domain.PlatformZhipu, Credentials: map[string]any{"account_mode": AccountModeCoding}},
		},
		{
			name:    "zhipu payg has no balance endpoint",
			account: &Account{ID: 5, Platform: domain.PlatformZhipu},
			wantErr: ErrChannelMonitorAccountNotSupportable,
		},
		{
			name:    "kimi payg ok",
			account: &Account{ID: 6, Platform: domain.PlatformKimi},
		},
		{
			name:    "deepseek payg ok",
			account: &Account{ID: 7, Platform: domain.PlatformDeepseek},
		},
		{
			name:    "anthropic api key cannot query usage",
			account: &Account{ID: 8, Platform: domain.PlatformAnthropic, Type: AccountTypeAPIKey},
			wantErr: ErrChannelMonitorAccountNotSupportable,
		},
		{
			name:    "anthropic oauth ok",
			account: &Account{ID: 9, Platform: domain.PlatformAnthropic, Type: AccountTypeOAuth},
		},
		{
			name:    "anthropic setup token ok (local estimation)",
			account: &Account{ID: 10, Platform: domain.PlatformAnthropic, Type: AccountTypeSetupToken},
		},
		{
			name:    "openai api key cannot query usage",
			account: &Account{ID: 11, Platform: domain.PlatformOpenAI, Type: AccountTypeAPIKey},
			wantErr: ErrChannelMonitorAccountNotSupportable,
		},
		{
			name:    "openai oauth ok",
			account: &Account{ID: 12, Platform: domain.PlatformOpenAI, Type: AccountTypeOAuth},
		},
		{
			// 防过度拦截：gemini/grok/antigravity 走本地统计/值通道降级，不会永久 error。
			name:    "gemini api key ok",
			account: &Account{ID: 13, Platform: domain.PlatformGemini, Type: AccountTypeAPIKey},
		},
		{
			name:    "grok ok",
			account: &Account{ID: 14, Platform: domain.PlatformGrok},
		},
		{
			name:    "antigravity ok",
			account: &Account{ID: 15, Platform: domain.PlatformAntigravity},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := monitorAccountQuotaCapability(tc.account)
			if tc.wantErr == nil {
				require.NoError(t, err)
			} else {
				require.ErrorIs(t, err, tc.wantErr)
			}
		})
	}
}

func TestValidateLinkedAccount_CapabilityRejected(t *testing.T) {
	svc := NewChannelMonitorService(nil, nil)
	svc.SetQuotaFetcher(newQuotaModeFetcher(map[int64]*Account{
		1: {ID: 1, Platform: domain.PlatformDeepseek, Credentials: map[string]any{"account_mode": AccountModeCoding}},
	}, nil))

	err := svc.validateLinkedAccount(context.Background(), MonitorProviderDeepseek, int64Ptr(1))
	require.ErrorIs(t, err, ErrChannelMonitorAccountNotSupportable)
}

func TestRevalidateLinkedAccount_Capability(t *testing.T) {
	svc := NewChannelMonitorService(nil, nil)
	svc.SetQuotaFetcher(newQuotaModeFetcher(map[int64]*Account{
		2: {ID: 2, Platform: domain.PlatformDeepseek, Credentials: map[string]any{"account_mode": AccountModeCoding}},
	}, nil))

	quota := &ChannelMonitor{Provider: MonitorProviderDeepseek, CheckMode: MonitorCheckModeQuota, AccountID: int64Ptr(2)}
	require.ErrorIs(t, svc.revalidateLinkedAccount(context.Background(), quota), ErrChannelMonitorAccountNotSupportable)
	require.NotNil(t, quota.AccountID, "quota mode keeps the binding for the admin to fix")

	probe := &ChannelMonitor{Provider: MonitorProviderDeepseek, CheckMode: MonitorCheckModeProbe, AccountID: int64Ptr(2)}
	require.NoError(t, svc.revalidateLinkedAccount(context.Background(), probe))
	require.Nil(t, probe.AccountID, "probe mode should silently unbind unusable account")
}

// provider-only 更新不得绕过 provider × check_mode 组合校验。
func TestApplyMonitorUpdate_ProviderOnlyRevalidatesCheckMode(t *testing.T) {
	probeKimi := func() *ChannelMonitor {
		return &ChannelMonitor{
			Provider: MonitorProviderKimi, APIMode: MonitorAPIModeChatCompletions,
			Endpoint: "https://api.kimi.com", PrimaryModel: "kimi-k2",
			CheckMode: MonitorCheckModeProbe,
		}
	}

	provider := MonitorProviderAntigravity
	err := applyMonitorUpdate(probeKimi(), ChannelMonitorUpdateParams{Provider: &provider})
	require.ErrorIs(t, err, ErrChannelMonitorInvalidCheckMode)

	// 带上 check_mode/account_id 的完整切换合法。
	accountID := int64(3)
	err = applyMonitorUpdate(probeKimi(), ChannelMonitorUpdateParams{
		Provider: &provider, CheckMode: strPtr(MonitorCheckModeQuota), AccountID: &accountID,
	})
	require.NoError(t, err)

	// 存量非法行（antigravity+probe）仅改名/停用不被砖化。
	legacy := &ChannelMonitor{
		Provider: MonitorProviderAntigravity, APIMode: MonitorAPIModeChatCompletions,
		Endpoint: "https://example.com", PrimaryModel: "gemini-3-pro",
		CheckMode: MonitorCheckModeProbe,
	}
	newName := "renamed"
	require.NoError(t, applyMonitorUpdate(legacy, ChannelMonitorUpdateParams{Name: &newName}))
}

// --- quota → probe 切换的 key 管控（validateProbeAPIKey） ---

func TestValidateProbeAPIKey_QuotaToProbeRequiresFreshKey(t *testing.T) {
	svc := NewChannelMonitorService(nil, &duplicateChannelMonitorEncryptor{})

	quota := &ChannelMonitor{Provider: MonitorProviderKimi, CheckMode: MonitorCheckModeQuota, APIKey: "NEW:"}
	require.NoError(t, svc.validateProbeAPIKey(quota, "")) // quota 模式不管 key

	quota.CheckMode = MonitorCheckModeProbe
	// 存量密文解出空明文（quota 监控存的加密空串）→ 必须重填 key。
	require.ErrorIs(t, svc.validateProbeAPIKey(quota, ""), ErrChannelMonitorMissingAPIKey)
	// 提供新明文 key → 放行。
	require.NoError(t, svc.validateProbeAPIKey(quota, "sk-fresh"))
	// 密文解出非空明文 → 放行。
	require.NoError(t, svc.validateProbeAPIKey(
		&ChannelMonitor{Provider: MonitorProviderKimi, CheckMode: MonitorCheckModeProbe, APIKey: "OLD:sk-live"}, ""))
}

// --- Duplicate：quota 模式空明文重加密 ---

func TestDuplicateChannelMonitorQuotaModeReencryptsEmptyKey(t *testing.T) {
	accountID := int64(9)
	source := &ChannelMonitor{
		ID:              42,
		Name:            "kimi-quota",
		Provider:        MonitorProviderKimi,
		APIMode:         MonitorAPIModeChatCompletions,
		Endpoint:        "",
		APIKey:          "OLD:", // 解密为空串（quota 监控的加密空 key）
		PrimaryModel:    "quota",
		Enabled:         true,
		IntervalSeconds: 60,
		CheckMode:       MonitorCheckModeQuota,
		AccountID:       &accountID,
	}
	repo := &duplicateChannelMonitorRepoStub{source: source}
	service := NewChannelMonitorService(repo, &duplicateChannelMonitorEncryptor{})

	dup, err := service.Duplicate(context.Background(), 42, 7, "admin:7", "op-1")
	require.NoError(t, err)
	require.Equal(t, MonitorCheckModeQuota, dup.CheckMode)
	require.NotNil(t, dup.AccountID)
	require.Equal(t, accountID, *dup.AccountID)
	require.Empty(t, dup.APIKey, "plaintext stays empty for quota monitors")
	require.Len(t, repo.created, 1)
	require.Equal(t, "NEW:", repo.created[0].APIKey, "empty key must be re-encrypted, not dropped")
}
