package app

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"sub2api-cn-relay-manager/internal/pack"
	"sub2api-cn-relay-manager/internal/provision"
	"sub2api-cn-relay-manager/internal/store/sqlite"
	"sub2api-cn-relay-manager/internal/testutil"
)

func TestRunReconcileBackgroundSweepCreatesReconcileRunForLatestSuccessfulBatch(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(newBatchImportActionStubServer(t))
	defer server.Close()

	store := openReconcileBackgroundTestStore(t)
	defer closeAppTestStore(t, store)

	batchID, hostPK, _ := seedReconcileBackgroundRuntimeImport(t, store, server.URL)

	if err := runReconcileBackgroundSweep(context.Background(), store, 10*time.Minute, time.Now()); err != nil {
		t.Fatalf("runReconcileBackgroundSweep() error = %v", err)
	}

	providers, err := store.Providers().ListByProviderID(context.Background(), "deepseek")
	if err != nil {
		t.Fatalf("Providers().ListByProviderID() error = %v", err)
	}
	runs, err := store.ReconcileRuns().GetByProviderIDAndHostID(context.Background(), providers[0].ID, hostPK)
	if err != nil {
		t.Fatalf("ReconcileRuns().GetByProviderIDAndHostID() error = %v", err)
	}
	if len(runs) != 1 {
		t.Fatalf("reconcile runs = %d, want 1", len(runs))
	}
	if runs[0].BatchID != batchID {
		t.Fatalf("reconcile batch_id = %d, want %d", runs[0].BatchID, batchID)
	}
	if runs[0].Status == "" {
		t.Fatal("reconcile status = empty, want persisted result")
	}
}

func TestRunReconcileBackgroundSweepSkipsRecentReconcileRun(t *testing.T) {
	t.Parallel()

	server := httptest.NewServer(newBatchImportActionStubServer(t))
	defer server.Close()

	store := openReconcileBackgroundTestStore(t)
	defer closeAppTestStore(t, store)

	batchID, hostPK, providerPK := seedReconcileBackgroundRuntimeImport(t, store, server.URL)
	if _, err := store.ReconcileRuns().Create(context.Background(), sqlite.ReconcileRun{
		BatchID:     batchID,
		HostID:      hostPK,
		ProviderID:  providerPK,
		Status:      "active",
		SummaryJSON: `{"seed":true}`,
	}); err != nil {
		t.Fatalf("ReconcileRuns().Create() error = %v", err)
	}

	if err := runReconcileBackgroundSweep(context.Background(), store, 10*time.Minute, time.Now()); err != nil {
		t.Fatalf("runReconcileBackgroundSweep() error = %v", err)
	}

	runs, err := store.ReconcileRuns().GetByProviderIDAndHostID(context.Background(), providerPK, hostPK)
	if err != nil {
		t.Fatalf("ReconcileRuns().GetByProviderIDAndHostID() error = %v", err)
	}
	if len(runs) != 1 {
		t.Fatalf("reconcile runs = %d, want 1 recent run only", len(runs))
	}
}

func openReconcileBackgroundTestStore(t *testing.T) *sqlite.DB {
	t.Helper()

	store := testutil.OpenSQLiteStore(t, testutil.SQLiteTestDSN(t, "state.db", true))
	if _, err := store.SQLDB().Exec("PRAGMA foreign_keys = OFF"); err != nil {
		t.Fatalf("disable foreign keys pragma error = %v", err)
	}
	return store
}

func seedReconcileBackgroundRuntimeImport(t *testing.T, store *sqlite.DB, baseURL string) (int64, int64, int64) {
	t.Helper()

	hostPK, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:              "host-1",
		BaseURL:             baseURL,
		HostVersion:         "0.1.126",
		CapabilityProbeJSON: "{}",
		AuthType:            "apikey",
		AuthToken:           "host-token",
	})
	if err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}

	client, err := newSub2APIClient(baseURL, CreateHostAuth{Type: "apikey", Token: "host-token"})
	if err != nil {
		t.Fatalf("newSub2APIClient() error = %v", err)
	}

	loadedPack := pack.LoadedPack{
		Manifest: pack.Manifest{
			PackID:         "openai-cn-pack",
			Version:        "1.0.0",
			Vendor:         "OpenAI CN",
			TargetHost:     "sub2api",
			MinHostVersion: "0.1.126",
			MaxHostVersion: "0.2.x",
		},
		Providers: []pack.ProviderManifest{{
			ProviderID:     "deepseek",
			DisplayName:    "DeepSeek",
			BaseURL:        "https://api.deepseek.example",
			Platform:       "openai",
			AccountType:    "openai",
			DefaultModels:  []string{"kimi-k2.6"},
			SmokeTestModel: "kimi-k2.6",
			GroupTemplate:  pack.GroupTemplate{Name: "DeepSeek 默认分组", RateMultiplier: 1},
			ChannelTemplate: pack.ChannelTemplate{
				Name:         "DeepSeek 默认渠道",
				ModelMapping: map[string]string{"kimi-k2.6": "kimi-k2.6"},
			},
			PlanTemplate: pack.PlanTemplate{Name: "DeepSeek 默认套餐", Price: 0, ValidityDays: 30, ValidityUnit: "day"},
			Import:       pack.ImportOptions{SupportsMultiKey: true, SupportsStrict: true, SupportsPartial: true},
		}},
		Checksum: "checksum-1",
	}

	result, err := provision.NewRuntimeImportService(store, client).Import(context.Background(), provision.RuntimeImportRequest{
		HostID:      "host-1",
		HostBaseURL: baseURL,
		Pack:        loadedPack,
		Provider:    loadedPack.Providers[0],
		Mode:        provision.ImportModePartial,
		Keys:        []string{"entry-key"},
		Access: provision.AccessRequest{
			Mode:        provision.AccessModeSelfService,
			ProbeAPIKey: "gateway-key",
		},
	})
	if err != nil {
		t.Fatalf("RuntimeImportService.Import() error = %v", err)
	}

	packRow, err := store.Packs().GetByPackID(context.Background(), loadedPack.Manifest.PackID)
	if err != nil {
		t.Fatalf("Packs().GetByPackID() error = %v", err)
	}
	providerRow, err := store.Providers().GetByPackIDAndProviderID(context.Background(), packRow.ID, loadedPack.Providers[0].ProviderID)
	if err != nil {
		t.Fatalf("Providers().GetByPackIDAndProviderID() error = %v", err)
	}

	return result.BatchID, hostPK, providerRow.ID
}

func TestRunReconcileBackgroundSweepRequiresStore(t *testing.T) {
	t.Parallel()

	err := runReconcileBackgroundSweep(context.Background(), nil, time.Minute, time.Now())
	if err == nil || err.Error() != "store is required" {
		t.Fatalf("runReconcileBackgroundSweep() error = %v, want store is required", err)
	}
}

func TestRunReconcileBackgroundSweepReturnsContextErrorBeforeCandidateRun(t *testing.T) {
	t.Parallel()

	store := openReconcileBackgroundTestStore(t)
	defer closeAppTestStore(t, store)

	seedReconcileBackgroundBatch(t, store)
	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	err := runReconcileBackgroundSweep(ctx, store, time.Minute, time.Now())
	if !errors.Is(err, context.Canceled) {
		t.Fatalf("runReconcileBackgroundSweep() error = %v, want wrapped %v", err, context.Canceled)
	}
}

func TestRunReconcileBackgroundSweepReturnsJoinedCandidateErrors(t *testing.T) {
	t.Parallel()

	store := openReconcileBackgroundTestStore(t)
	defer closeAppTestStore(t, store)

	batchID, _, _ := seedReconcileBackgroundBatch(t, store)

	err := runReconcileBackgroundSweep(context.Background(), store, time.Minute, time.Now())
	if err == nil {
		t.Fatal("runReconcileBackgroundSweep() error = nil, want candidate failure")
	}
	want := fmt.Sprintf("run reconcile for batch %d: access closure not found for batch %d", batchID, batchID)
	if !strings.Contains(err.Error(), want) {
		t.Fatalf("runReconcileBackgroundSweep() error = %v, want contains %q", err, want)
	}
}

func TestReconcileRunDue(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, 5, 23, 10, 0, 0, 0, time.UTC)
	tests := []struct {
		name     string
		run      *sqlite.ReconcileRun
		interval time.Duration
		want     bool
	}{
		{name: "nil run", run: nil, interval: time.Minute, want: true},
		{name: "non positive interval", run: &sqlite.ReconcileRun{CreatedAt: "2026-05-23 09:59:59"}, interval: 0, want: true},
		{name: "invalid timestamp", run: &sqlite.ReconcileRun{CreatedAt: "bad-time"}, interval: time.Minute, want: true},
		{name: "recent run not due", run: &sqlite.ReconcileRun{CreatedAt: "2026-05-23 09:59:30"}, interval: time.Minute, want: false},
		{name: "old run due", run: &sqlite.ReconcileRun{CreatedAt: "2026-05-23 09:58:00"}, interval: time.Minute, want: true},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			if got := reconcileRunDue(now, tc.run, tc.interval); got != tc.want {
				t.Fatalf("reconcileRunDue() = %v, want %v", got, tc.want)
			}
		})
	}
}

func TestParseAccessClosureDetailsReturnsEmptyMapForInvalidJSON(t *testing.T) {
	t.Parallel()

	got := parseAccessClosureDetails("{")
	if len(got) != 0 {
		t.Fatalf("parseAccessClosureDetails() = %#v, want empty map", got)
	}
}

func TestParseJSONStringArrayAndParseJSONInt(t *testing.T) {
	t.Parallel()

	values := parseJSONStringArray([]any{" user-1 ", 42, "", "user-2"})
	if len(values) != 2 || values[0] != "user-1" || values[1] != "user-2" {
		t.Fatalf("parseJSONStringArray() = %v, want [user-1 user-2]", values)
	}
	if got := parseJSONStringArray("wrong-type"); got != nil {
		t.Fatalf("parseJSONStringArray(wrong-type) = %v, want nil", got)
	}
	if got := parseJSONInt(float64(30)); got != 30 {
		t.Fatalf("parseJSONInt(float64) = %d, want 30", got)
	}
	if got := parseJSONInt(15); got != 15 {
		t.Fatalf("parseJSONInt(int) = %d, want 15", got)
	}
	if got := parseJSONInt("30"); got != 0 {
		t.Fatalf("parseJSONInt(string) = %d, want 0", got)
	}
}

func TestStoredLoadedPackFallsBackToColumns(t *testing.T) {
	t.Parallel()

	loaded, err := storedLoadedPack(sqlite.Pack{
		PackID:         "openai-cn-pack",
		Version:        "1.0.0",
		Checksum:       "checksum-1",
		Vendor:         "OpenAI CN",
		TargetHost:     "sub2api",
		MinHostVersion: "0.1.126",
		MaxHostVersion: "0.2.x",
		ManifestJSON:   "{}",
	})
	if err != nil {
		t.Fatalf("storedLoadedPack() error = %v", err)
	}
	if loaded.Manifest.PackID != "openai-cn-pack" || loaded.Manifest.TargetHost != "sub2api" || loaded.Checksum != "checksum-1" {
		t.Fatalf("storedLoadedPack() = %+v, want fallback fields populated", loaded)
	}
}

func TestStoredLoadedPackRejectsInvalidManifestJSON(t *testing.T) {
	t.Parallel()

	_, err := storedLoadedPack(sqlite.Pack{ManifestJSON: "{"})
	if err == nil || !strings.Contains(err.Error(), "decode stored pack manifest") {
		t.Fatalf("storedLoadedPack() error = %v, want decode stored pack manifest", err)
	}
}

func TestStoredProviderManifestFallsBackToColumns(t *testing.T) {
	t.Parallel()

	provider, err := storedProviderManifest(sqlite.Provider{
		ProviderID:     "deepseek",
		DisplayName:    "DeepSeek",
		BaseURL:        "https://api.example.com",
		Platform:       "openai",
		AccountType:    "openai",
		SmokeTestModel: "deepseek-chat",
		ManifestJSON:   "{}",
	})
	if err != nil {
		t.Fatalf("storedProviderManifest() error = %v", err)
	}
	if provider.ProviderID != "deepseek" || provider.AccountType != "openai" || provider.SmokeTestModel != "deepseek-chat" {
		t.Fatalf("storedProviderManifest() = %+v, want fallback fields populated", provider)
	}
}

func TestStoredProviderManifestRejectsInvalidManifestJSON(t *testing.T) {
	t.Parallel()

	_, err := storedProviderManifest(sqlite.Provider{ManifestJSON: "{"})
	if err == nil || !strings.Contains(err.Error(), "decode stored provider manifest") {
		t.Fatalf("storedProviderManifest() error = %v, want decode stored provider manifest", err)
	}
}

func TestResolveManagedResourceHostIDByBatch(t *testing.T) {
	t.Parallel()

	store := openReconcileBackgroundTestStore(t)
	defer closeAppTestStore(t, store)

	batchID, hostPK, _ := seedReconcileBackgroundBatch(t, store)
	if _, err := store.ManagedResources().Create(context.Background(), sqlite.ManagedResource{
		BatchID:        batchID,
		HostID:         hostPK,
		ResourceType:   "group",
		HostResourceID: "group_1",
		ResourceName:   "group one",
	}); err != nil {
		t.Fatalf("ManagedResources().Create() error = %v", err)
	}

	groupID, err := resolveManagedResourceHostIDByBatch(context.Background(), store, batchID, "group")
	if err != nil {
		t.Fatalf("resolveManagedResourceHostIDByBatch() error = %v", err)
	}
	if groupID != "group_1" {
		t.Fatalf("groupID = %q, want group_1", groupID)
	}
	if _, err := resolveManagedResourceHostIDByBatch(context.Background(), store, batchID, "plan"); err == nil || err.Error() != fmt.Sprintf("managed resource %q not found for batch %d", "plan", batchID) {
		t.Fatalf("resolveManagedResourceHostIDByBatch(plan) error = %v, want missing resource error", err)
	}
}

func TestReconcileProbeAPIKeySelfService(t *testing.T) {
	t.Parallel()

	store := openReconcileBackgroundTestStore(t)
	defer closeAppTestStore(t, store)

	batchID, _, _ := seedReconcileBackgroundBatch(t, store)
	hostRow := mustGetBackgroundHost(t, store)
	tests := []struct {
		name       string
		record     sqlite.AccessClosureRecord
		wantAPIKey string
		wantErr    string
	}{
		{
			name: "prefers access api key",
			record: sqlite.AccessClosureRecord{
				BatchID:     batchID,
				ClosureType: provision.AccessModeSelfService,
				Status:      "self_service_ready",
				DetailsJSON: `{"access_api_key":" access-key ","probe_api_key":"probe-key"}`,
			},
			wantAPIKey: "access-key",
		},
		{
			name: "falls back to probe api key",
			record: sqlite.AccessClosureRecord{
				BatchID:     batchID,
				ClosureType: provision.AccessModeSelfService,
				Status:      "self_service_ready",
				DetailsJSON: `{"probe_api_key":" probe-key "}`,
			},
			wantAPIKey: "probe-key",
		},
		{
			name: "requires api key",
			record: sqlite.AccessClosureRecord{
				BatchID:     batchID,
				ClosureType: provision.AccessModeSelfService,
				Status:      "self_service_ready",
				DetailsJSON: `{}`,
			},
			wantErr: "self_service access closure missing probe api key",
		},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got, err := reconcileProbeAPIKey(context.Background(), store, hostRow, sqlite.ImportBatch{ID: batchID}, []sqlite.AccessClosureRecord{tc.record})
			if tc.wantErr != "" {
				if err == nil || err.Error() != tc.wantErr {
					t.Fatalf("reconcileProbeAPIKey() error = %v, want %q", err, tc.wantErr)
				}
				return
			}
			if err != nil {
				t.Fatalf("reconcileProbeAPIKey() error = %v", err)
			}
			if got != tc.wantAPIKey {
				t.Fatalf("reconcileProbeAPIKey() = %q, want %q", got, tc.wantAPIKey)
			}
		})
	}
}

func TestReconcileProbeAPIKeyRejectsMissingSubscriptionUsers(t *testing.T) {
	t.Parallel()

	store := openReconcileBackgroundTestStore(t)
	defer closeAppTestStore(t, store)

	batchID, _, _ := seedReconcileBackgroundBatch(t, store)
	hostRow := mustGetBackgroundHost(t, store)

	_, err := reconcileProbeAPIKey(context.Background(), store, hostRow, sqlite.ImportBatch{ID: batchID}, []sqlite.AccessClosureRecord{{
		BatchID:     batchID,
		ClosureType: provision.AccessModeSubscription,
		Status:      "subscription_ready",
		DetailsJSON: `{}`,
	}})
	if err == nil || err.Error() != "subscription access closure missing subscription_users" {
		t.Fatalf("reconcileProbeAPIKey() error = %v, want missing subscription_users", err)
	}
}

func TestReconcileProbeAPIKeyRejectsUnsupportedClosureType(t *testing.T) {
	t.Parallel()

	store := openReconcileBackgroundTestStore(t)
	defer closeAppTestStore(t, store)

	batchID, _, _ := seedReconcileBackgroundBatch(t, store)
	hostRow := mustGetBackgroundHost(t, store)

	_, err := reconcileProbeAPIKey(context.Background(), store, hostRow, sqlite.ImportBatch{ID: batchID}, []sqlite.AccessClosureRecord{{
		BatchID:     batchID,
		ClosureType: "other",
		Status:      "unknown",
	}})
	if err == nil || err.Error() != `unsupported access closure type "other"` {
		t.Fatalf("reconcileProbeAPIKey() error = %v, want unsupported type", err)
	}
}

func TestReconcileProbeAPIKeyRequiresAccessClosure(t *testing.T) {
	t.Parallel()

	store := openReconcileBackgroundTestStore(t)
	defer closeAppTestStore(t, store)

	batchID, _, _ := seedReconcileBackgroundBatch(t, store)
	hostRow := mustGetBackgroundHost(t, store)

	_, err := reconcileProbeAPIKey(context.Background(), store, hostRow, sqlite.ImportBatch{ID: batchID}, nil)
	if err == nil || err.Error() != fmt.Sprintf("access closure not found for batch %d", batchID) {
		t.Fatalf("reconcileProbeAPIKey() error = %v, want missing access closure", err)
	}
}

func TestReconcileProbeAPIKeySubscriptionSuccess(t *testing.T) {
	t.Parallel()

	var assignCalls int
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.Method == http.MethodGet && strings.HasPrefix(r.URL.RequestURI(), "/api/v1/admin/users?"):
			writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"items": []map[string]any{}}})
		case r.Method == http.MethodPost && r.URL.Path == "/api/v1/admin/users":
			writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": 84, "email": "relay-sub-user-1@sub2api.local"}})
		case r.Method == http.MethodPut && r.URL.Path == "/api/v1/admin/users/84":
			writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": 84}})
		case r.Method == http.MethodPost && r.URL.Path == "/api/v1/admin/users/84/balance":
			writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": 84}})
		case r.Method == http.MethodPost && r.URL.Path == "/api/v1/admin/subscriptions/assign":
			assignCalls++
			writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": 401}})
		case r.Method == http.MethodPost && r.URL.Path == "/api/v1/auth/login":
			writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"access_token": "user-jwt"}})
		case r.Method == http.MethodPost && r.URL.Path == "/api/v1/keys":
			writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": 501, "key": "sk-relay-key", "name": "managed-key"}})
		case r.Method == http.MethodPut && r.URL.Path == "/api/v1/admin/api-keys/501":
			writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"api_key": map[string]any{"id": 501}}})
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	store := openReconcileBackgroundTestStore(t)
	defer closeAppTestStore(t, store)

	hostPK, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:              "host-subscription",
		BaseURL:             server.URL,
		HostVersion:         "0.1.126",
		CapabilityProbeJSON: "{}",
		AuthType:            "bearer",
		AuthToken:           "admin-token",
	})
	if err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	packPK := createBackgroundPack(t, store)
	providerPK := createBackgroundProvider(t, store, packPK)
	batchID, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       hostPK,
		PackID:       packPK,
		ProviderID:   providerPK,
		Mode:         provision.ImportModePartial,
		BatchStatus:  provision.BatchStatusSucceeded,
		AccessStatus: provision.AccessStatusSubscriptionReady,
	})
	if err != nil {
		t.Fatalf("ImportBatches().Create() error = %v", err)
	}
	if _, err := store.ManagedResources().Create(context.Background(), sqlite.ManagedResource{
		BatchID:        batchID,
		HostID:         hostPK,
		ResourceType:   "group",
		HostResourceID: "101",
		ResourceName:   "group-101",
	}); err != nil {
		t.Fatalf("ManagedResources().Create(group) error = %v", err)
	}

	hostRow := sqlite.Host{HostID: "host-subscription", BaseURL: server.URL, AuthType: "bearer", AuthToken: "admin-token"}
	got, err := reconcileProbeAPIKey(context.Background(), store, hostRow, sqlite.ImportBatch{ID: batchID}, []sqlite.AccessClosureRecord{{
		BatchID:     batchID,
		ClosureType: provision.AccessModeSubscription,
		Status:      provision.AccessStatusSubscriptionReady,
		DetailsJSON: `{"subscription_users":["crm-user-1"],"subscription_days":0}`,
	}})
	if err != nil {
		t.Fatalf("reconcileProbeAPIKey() error = %v", err)
	}
	if !strings.HasPrefix(got, "sk-relay-") {
		t.Fatalf("reconcileProbeAPIKey() = %q, want sk-relay-*", got)
	}
	if assignCalls != 1 {
		t.Fatalf("subscription assign calls = %d, want 1 (EnsureSubscriptionAccess only)", assignCalls)
	}
}

func TestReconcileProbeAPIKeySubscriptionRequiresHostAuth(t *testing.T) {
	t.Parallel()

	store := openReconcileBackgroundTestStore(t)
	defer closeAppTestStore(t, store)

	hostPK, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:              "host-subscription",
		BaseURL:             "https://sub2api.example.com",
		HostVersion:         "0.1.126",
		CapabilityProbeJSON: "{}",
		AuthType:            "bearer",
		AuthToken:           "admin-token",
	})
	if err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	packPK := createBackgroundPack(t, store)
	providerPK := createBackgroundProvider(t, store, packPK)
	batchID, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       hostPK,
		PackID:       packPK,
		ProviderID:   providerPK,
		Mode:         provision.ImportModePartial,
		BatchStatus:  provision.BatchStatusSucceeded,
		AccessStatus: provision.AccessStatusSubscriptionReady,
	})
	if err != nil {
		t.Fatalf("ImportBatches().Create() error = %v", err)
	}
	if _, err := store.ManagedResources().Create(context.Background(), sqlite.ManagedResource{
		BatchID:        batchID,
		HostID:         hostPK,
		ResourceType:   "group",
		HostResourceID: "101",
		ResourceName:   "group-101",
	}); err != nil {
		t.Fatalf("ManagedResources().Create(group) error = %v", err)
	}

	_, err = reconcileProbeAPIKey(context.Background(), store, sqlite.Host{BaseURL: "https://sub2api.example.com", AuthType: "bearer"}, sqlite.ImportBatch{ID: batchID}, []sqlite.AccessClosureRecord{{
		BatchID:     batchID,
		ClosureType: provision.AccessModeSubscription,
		Status:      provision.AccessStatusSubscriptionReady,
		DetailsJSON: `{"subscription_users":["crm-user-1"],"subscription_days":30}`,
	}})
	if err == nil || !strings.Contains(err.Error(), "auth.token is required") {
		t.Fatalf("reconcileProbeAPIKey() error = %v, want auth.token is required", err)
	}
}

func createBackgroundPack(t *testing.T, store *sqlite.DB) int64 {
	t.Helper()

	packPK, err := store.Packs().Create(context.Background(), sqlite.Pack{
		PackID:         "openai-cn-pack",
		Version:        "1.0.0",
		Checksum:       "checksum-1",
		Vendor:         "OpenAI CN",
		TargetHost:     "sub2api",
		MinHostVersion: "0.1.126",
		MaxHostVersion: "0.2.x",
		ManifestJSON:   `{"pack_id":"openai-cn-pack","version":"1.0.0","target_host":"sub2api"}`,
	})
	if err != nil {
		t.Fatalf("Packs().Create() error = %v", err)
	}
	return packPK
}

func createBackgroundProvider(t *testing.T, store *sqlite.DB, packPK int64) int64 {
	t.Helper()

	providerPK, err := store.Providers().Create(context.Background(), sqlite.Provider{
		PackID:         packPK,
		ProviderID:     "deepseek",
		DisplayName:    "DeepSeek",
		BaseURL:        "https://api.example.com",
		Platform:       "openai",
		AccountType:    "openai",
		SmokeTestModel: "deepseek-chat",
		ManifestJSON:   `{"provider_id":"deepseek","base_url":"https://api.example.com","platform":"openai","account_type":"openai","smoke_test_model":"deepseek-chat"}`,
	})
	if err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}
	return providerPK
}

func mustGetBackgroundHost(t *testing.T, store *sqlite.DB) sqlite.Host {
	t.Helper()

	host, err := store.Hosts().GetByHostID(context.Background(), "host-1")
	if err != nil {
		t.Fatalf("Hosts().GetByHostID() error = %v", err)
	}
	return host
}

func seedReconcileBackgroundBatch(t *testing.T, store *sqlite.DB) (int64, int64, int64) {
	t.Helper()

	hostPK, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:              "host-1",
		BaseURL:             "https://sub2api.example.com",
		HostVersion:         "0.1.126",
		CapabilityProbeJSON: "{}",
		AuthType:            "apikey",
		AuthToken:           "host-token",
	})
	if err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	packPK, err := store.Packs().Create(context.Background(), sqlite.Pack{
		PackID:         "openai-cn-pack",
		Version:        "1.0.0",
		Checksum:       "checksum-1",
		Vendor:         "OpenAI CN",
		TargetHost:     "sub2api",
		MinHostVersion: "0.1.126",
		MaxHostVersion: "0.2.x",
		ManifestJSON:   `{"pack_id":"openai-cn-pack","version":"1.0.0","target_host":"sub2api"}`,
	})
	if err != nil {
		t.Fatalf("Packs().Create() error = %v", err)
	}
	providerPK, err := store.Providers().Create(context.Background(), sqlite.Provider{
		PackID:         packPK,
		ProviderID:     "deepseek",
		DisplayName:    "DeepSeek",
		BaseURL:        "https://api.example.com",
		Platform:       "openai",
		AccountType:    "openai",
		SmokeTestModel: "deepseek-chat",
		ManifestJSON:   `{"provider_id":"deepseek","base_url":"https://api.example.com","platform":"openai","account_type":"openai","smoke_test_model":"deepseek-chat"}`,
	})
	if err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}
	batchID, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       hostPK,
		PackID:       packPK,
		ProviderID:   providerPK,
		Mode:         provision.ImportModePartial,
		BatchStatus:  "partially_succeeded",
		AccessStatus: "self_service_ready",
	})
	if err != nil {
		t.Fatalf("ImportBatches().Create() error = %v", err)
	}
	return batchID, hostPK, providerPK
}
