package app

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"sub2api-cn-relay-manager/internal/batch"
	"sub2api-cn-relay-manager/internal/config"
	"sub2api-cn-relay-manager/internal/host/sub2api"
	"sub2api-cn-relay-manager/internal/provision"
	"sub2api-cn-relay-manager/internal/store/sqlite"
)

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

	if got := (batchImportResumeJob{}).Name(); got != "batch import runtime scheduler" {
		t.Fatalf("batchImportResumeJob.Name() = %q, want batch import runtime scheduler", got)
	}
	values := parseJSONStringList(`[" user-1 ","user-2"]`)
	if len(values) != 2 || values[0] != " user-1 " || values[1] != "user-2" {
		t.Fatalf("parseJSONStringList() = %v, want raw decoded values", values)
	}
	if got := parseJSONStringList("{"); len(got) != 0 {
		t.Fatalf("parseJSONStringList(invalid) = %v, want empty", got)
	}
}

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

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

	mustCreateAppImportRun(t, store, sqlite.ImportRun{
		RunID:      "run-1",
		HostID:     "host-1",
		Mode:       "partial",
		AccessMode: "self_service",
		State:      "completed",
	})
	job := batchImportResumeJob{sqliteDSN: appTestDSN(t, store)}
	if err := job.Run(context.Background()); err != nil {
		t.Fatalf("batchImportResumeJob.Run() error = %v", err)
	}
	if got := (reconcileSweepJob{}).Name(); got != "reconcile background scheduler" {
		t.Fatalf("reconcileSweepJob.Name() = %q, want reconcile background scheduler", got)
	}
}

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

	schedulers := defaultBackgroundSchedulers()
	if schedulers.runBatchImport == nil || schedulers.runReconcile == nil {
		t.Fatalf("defaultBackgroundSchedulers() = %+v, want non-nil functions", schedulers)
	}
	actions := NewActionSet("file:/tmp/nonexistent.db")
	if actions.CreateBatchImportRun == nil || actions.ListBatchImportRuns == nil || actions.GetBatchImportRun == nil || actions.ListBatchImportRunItems == nil || actions.GetBatchImportRunItem == nil {
		t.Fatalf("NewActionSet() returned nil batch actions: %+v", actions)
	}
	if actions.CreateHost == nil || actions.ListPacks == nil || actions.GetPack == nil || actions.ListPackProviders == nil || actions.PublishProviderDraft == nil {
		t.Fatalf("NewActionSet() returned nil app actions: %+v", actions)
	}
}

func TestStartBackgroundSchedulersAndBootstrap(t *testing.T) {
	var batchCalls int
	var reconcileCalls int
	startBackgroundSchedulers(context.Background(), config.StartupConfig{
		Database: config.DatabaseConfig{SQLiteDSN: "file:test.db"},
		Reconcile: config.ReconcileConfig{
			WorkerEnabled: false,
		},
	}, backgroundSchedulers{
		runBatchImport: func(context.Context, string) { batchCalls++ },
		runReconcile:   func(context.Context, string, time.Duration) { reconcileCalls++ },
	})
	if batchCalls != 1 || reconcileCalls != 0 {
		t.Fatalf("startBackgroundSchedulers(disabled) calls = (%d, %d), want (1, 0)", batchCalls, reconcileCalls)
	}

	startBackgroundSchedulers(context.Background(), config.StartupConfig{
		Database: config.DatabaseConfig{SQLiteDSN: "file:test.db"},
		Reconcile: config.ReconcileConfig{
			WorkerEnabled: true,
			PollInterval:  time.Minute,
		},
	}, backgroundSchedulers{
		runBatchImport: func(context.Context, string) { batchCalls++ },
		runReconcile:   func(context.Context, string, time.Duration) { reconcileCalls++ },
	})
	if batchCalls != 2 || reconcileCalls != 1 {
		t.Fatalf("startBackgroundSchedulers(enabled) calls = (%d, %d), want (2, 1)", batchCalls, reconcileCalls)
	}

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	t.Setenv(config.EnvListenAddr, "127.0.0.1:19090")
	t.Setenv(config.EnvSQLiteDSN, "file:bootstrap-test.db?_foreign_keys=on&_busy_timeout=5000")
	t.Setenv(config.EnvAdminToken, "bootstrap-token")
	t.Setenv(config.EnvReconcileWorkerEnabled, "false")
	server, err := Bootstrap(ctx)
	if err != nil {
		t.Fatalf("Bootstrap() error = %v", err)
	}
	if server.Addr() != "127.0.0.1:19090" {
		t.Fatalf("Bootstrap() server.Addr() = %q, want 127.0.0.1:19090", server.Addr())
	}
}

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

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

	ctx, cancel := context.WithCancel(context.Background())
	cancel()

	runBatchImportBackgroundScheduler(ctx, appTestDSN(t, store))
	runReconcileBackgroundScheduler(ctx, appTestDSN(t, store), 0)

	job := reconcileSweepJob{sqliteDSN: appTestDSN(t, store), interval: time.Minute}
	if err := job.Run(context.Background()); err != nil {
		t.Fatalf("reconcileSweepJob.Run() error = %v", err)
	}
}

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

	if err := (batchImportProvisioner{}).Patch(context.Background(), batch.PatchProvisionRequest{}); err != nil {
		t.Fatalf("Patch() error = %v, want nil", err)
	}
	if err := sleepWithContext(context.Background(), 0); err != nil {
		t.Fatalf("sleepWithContext() error = %v, want nil", err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	if err := sleepWithContext(ctx, time.Second); err != context.Canceled {
		t.Fatalf("sleepWithContext(canceled) error = %v, want %v", err, context.Canceled)
	}
}

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

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

	client, err := newSub2APIClient(server.URL, CreateHostAuth{Type: "apikey", Token: "host-token"})
	if err != nil {
		t.Fatalf("newSub2APIClient() error = %v", err)
	}
	hostVersion, capabilities, err := probeHostSnapshot(context.Background(), client)
	if err != nil {
		t.Fatalf("probeHostSnapshot() error = %v", err)
	}
	if hostVersion != "0.1.126" || !capabilities.Groups || !capabilities.Subscriptions {
		t.Fatalf("probeHostSnapshot() = (%q, %+v), want supported host snapshot", hostVersion, capabilities)
	}

	selfServiceRunner := batchImportRuntimeRunner{
		request: CreateBatchImportRunRequest{
			AccessMode:  provision.AccessModeSelfService,
			ProbeAPIKey: " probe-key ",
		},
	}
	apiKey, err := selfServiceRunner.resolveValidationAPIKey(context.Background(), sqlite.ImportRunItem{})
	if err != nil {
		t.Fatalf("resolveValidationAPIKey(self_service) error = %v", err)
	}
	if apiKey != "probe-key" {
		t.Fatalf("resolveValidationAPIKey(self_service) = %q, want probe-key", apiKey)
	}

	subscriptionRunner := batchImportRuntimeRunner{
		request: CreateBatchImportRunRequest{
			AccessMode:       provision.AccessModeSubscription,
			SubscriptionDays: 30,
		},
	}
	if _, err := subscriptionRunner.resolveValidationAPIKey(context.Background(), sqlite.ImportRunItem{}); err == nil || err.Error() != "subscription_users is required" {
		t.Fatalf("resolveValidationAPIKey(subscription missing users) error = %v, want subscription_users is required", err)
	}

	unsupportedRunner := batchImportRuntimeRunner{
		request: CreateBatchImportRunRequest{
			AccessMode: "other",
		},
	}
	if _, err := unsupportedRunner.resolveValidationAPIKey(context.Background(), sqlite.ImportRunItem{}); err == nil || !strings.Contains(err.Error(), `unsupported access mode "other"`) {
		t.Fatalf("resolveValidationAPIKey(unsupported) error = %v, want unsupported access mode", err)
	}
}

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

	if _, err := resolveManagedResourceHostID(context.Background(), nil, sqlite.ImportRunItem{}, "account"); err == nil || err.Error() != "store is required" {
		t.Fatalf("resolveManagedResourceHostID(nil store) error = %v, want store is required", err)
	}
	if _, err := resolveManagedResourceHostID(context.Background(), openAppTestStore(t), sqlite.ImportRunItem{}, "account"); err == nil || err.Error() != "legacy_batch_id is required for account lookup" {
		t.Fatalf("resolveManagedResourceHostID(missing batch) error = %v, want legacy_batch_id required", err)
	}

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v1/admin/accounts/account-ok/test":
			w.Header().Set("Content-Type", "text/event-stream")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte("data: {\"type\":\"test_complete\",\"ok\":true,\"status\":\"passed\",\"message\":\"smoke passed\"}\n\n"))
		case "/api/v1/admin/accounts/account-http/test":
			w.WriteHeader(http.StatusForbidden)
			_, _ = w.Write([]byte(`{"error":"forbidden"}`))
		case "/api/v1/admin/accounts/account-busy/test":
			w.Header().Set("Content-Type", "text/event-stream")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte("data: {\"type\":\"test_complete\",\"ok\":false,\"status\":\"failed\",\"message\":\"No available accounts\"}\n\n"))
		case "/api/v1/admin/accounts/account-forbidden/test":
			w.Header().Set("Content-Type", "text/event-stream")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte("data: {\"type\":\"test_complete\",\"ok\":false,\"status\":\"failed\",\"message\":\"Forbidden by upstream\"}\n\n"))
		case "/api/v1/admin/accounts/account-bad/test":
			w.Header().Set("Content-Type", "text/event-stream")
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte("data: {\"type\":\"test_complete\",\"ok\":false,\"status\":\"failed\",\"message\":\"model mismatch\"}\n\n"))
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

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

	tests := []struct {
		name       string
		accountID  string
		wantStatus int
		wantMsg    string
	}{
		{name: "probe ok", accountID: "account-ok", wantStatus: http.StatusOK, wantMsg: "smoke passed"},
		{name: "http error passthrough", accountID: "account-http", wantStatus: http.StatusForbidden, wantMsg: `{"error":"forbidden"}`},
		{name: "busy advisory", accountID: "account-busy", wantStatus: http.StatusServiceUnavailable, wantMsg: "No available accounts"},
		{name: "forbidden advisory", accountID: "account-forbidden", wantStatus: http.StatusForbidden, wantMsg: "Forbidden by upstream"},
		{name: "generic bad request", accountID: "account-bad", wantStatus: http.StatusBadRequest, wantMsg: "model mismatch"},
	}

	for _, tc := range tests {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			store := openAppTestStore(t)
			defer closeAppTestStore(t, store)

			hostPK := createAppHostRecord(t, store, server.URL)
			packPK := createAppPackRecord(t, store)
			providerPK := createAppProviderRecord(t, store, packPK)
			batchID := createAppBatchRecord(t, store, hostPK, packPK, providerPK)
			if _, err := store.ManagedResources().Create(context.Background(), sqlite.ManagedResource{
				BatchID:        batchID,
				HostID:         hostPK,
				ResourceType:   "account",
				HostResourceID: tc.accountID,
				ResourceName:   tc.accountID,
			}); err != nil {
				t.Fatalf("ManagedResources().Create() error = %v", err)
			}

			runner := batchImportRuntimeRunner{
				store:      store,
				hostClient: client,
			}
			result, err := runner.confirmItem(context.Background(), sqlite.ImportRunItem{
				LegacyBatchID:      &batchID,
				ResolvedSmokeModel: "kimi-k2.6",
			})
			if err != nil {
				t.Fatalf("confirmItem() error = %v", err)
			}
			if result.StatusCode != tc.wantStatus || result.Message != tc.wantMsg {
				t.Fatalf("confirmItem() = %+v, want status=%d message=%q", result, tc.wantStatus, tc.wantMsg)
			}
		})
	}
}

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

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

	mustCreateAppImportRun(t, store, sqlite.ImportRun{
		RunID:      "run-lease-1",
		HostID:     "host-1",
		Mode:       "partial",
		AccessMode: "self_service",
		State:      "running",
	})
	mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
		ItemID:              "item-lease-1",
		RunID:               "run-lease-1",
		BaseURL:             "https://deepseek.example.com/v1",
		ProviderID:          "deepseek",
		APIKeyFingerprint:   "sha256:lease",
		ResolvedSmokeModel:  "deepseek-v4-pro",
		CurrentStage:        "confirm",
		ConfirmationStatus:  "pending",
		AccessStatus:        "unknown",
		MatchedAccountState: "active",
		AccountResolution:   "created",
	})

	leaseStore := batchImportRunItemStore{store: store, runID: "run-lease-1"}
	now := time.Date(2026, 5, 23, 10, 0, 0, 0, time.UTC)

	if _, _, err := leaseStore.TryAcquireLease(context.Background(), "", "worker-1", now, time.Minute); err == nil || err.Error() != "item_id is required" {
		t.Fatalf("TryAcquireLease(missing item) error = %v, want item_id is required", err)
	}
	if _, _, err := leaseStore.TryAcquireLease(context.Background(), "item-lease-1", "", now, time.Minute); err == nil || err.Error() != "worker_id is required" {
		t.Fatalf("TryAcquireLease(missing worker) error = %v, want worker_id is required", err)
	}

	item, claimed, err := leaseStore.TryAcquireLease(context.Background(), "item-lease-1", "worker-1", now, time.Minute)
	if err != nil {
		t.Fatalf("TryAcquireLease(first) error = %v", err)
	}
	if !claimed || item.LeaseOwner != "worker-1" {
		t.Fatalf("TryAcquireLease(first) = (%+v, %v), want claimed by worker-1", item, claimed)
	}

	item, claimed, err = leaseStore.TryAcquireLease(context.Background(), "item-lease-1", "worker-2", now.Add(30*time.Second), time.Minute)
	if err != nil {
		t.Fatalf("TryAcquireLease(second) error = %v", err)
	}
	if claimed || item.ItemID != "" {
		t.Fatalf("TryAcquireLease(second) = (%+v, %v), want unclaimed empty item", item, claimed)
	}
}

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

	t.Run("validate stage completes run", func(t *testing.T) {
		t.Parallel()

		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.URL.Path {
			case "/v1/chat/completions":
				writeJSON(w, http.StatusOK, map[string]any{
					"id": "chatcmpl_validate",
					"choices": []map[string]any{{
						"index": 0,
						"message": map[string]any{
							"role":    "assistant",
							"content": "pong",
						},
					}},
				})
			default:
				http.NotFound(w, r)
			}
		}))
		defer server.Close()

		client, err := newSub2APIClient(server.URL, CreateHostAuth{Type: "bearer", Token: "gateway-key"})
		if err != nil {
			t.Fatalf("newSub2APIClient() error = %v", err)
		}

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

		mustCreateAppImportRun(t, store, sqlite.ImportRun{
			RunID:      "run-validate-1",
			HostID:     "host-1",
			Mode:       "partial",
			AccessMode: "self_service",
			State:      "running",
			TotalItems: 1,
		})
		mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
			ItemID:              "item-validate-1",
			RunID:               "run-validate-1",
			BaseURL:             server.URL,
			ProviderID:          "deepseek",
			APIKeyFingerprint:   "sha256:validate",
			ResolvedSmokeModel:  "deepseek-v4-pro",
			CurrentStage:        "validate",
			ConfirmationStatus:  "confirmed",
			AccessStatus:        "unknown",
			MatchedAccountState: "active",
			AccountResolution:   "created",
		})

		runner := batchImportRuntimeRunner{
			store:      store,
			hostClient: client,
			request: CreateBatchImportRunRequest{
				AccessMode:  provision.AccessModeSelfService,
				ProbeAPIKey: "gateway-key",
			},
		}
		if err := runner.driveRun(context.Background(), "run-validate-1", 0); err != nil {
			t.Fatalf("driveRun(validate) error = %v", err)
		}

		run, err := store.ImportRuns().GetByRunID(context.Background(), "run-validate-1")
		if err != nil {
			t.Fatalf("ImportRuns().GetByRunID() error = %v", err)
		}
		if run.State != "completed" || run.CompletedItems != 1 || run.ActiveItems != 1 {
			t.Fatalf("run = %+v, want completed with one active item", run)
		}

		item, err := store.ImportRunItems().GetByItemID(context.Background(), "item-validate-1")
		if err != nil {
			t.Fatalf("ImportRunItems().GetByItemID() error = %v", err)
		}
		if item.CurrentStage != "done" || item.AccessStatus != "active" {
			t.Fatalf("item = %+v, want done/active", item)
		}
	})

	t.Run("confirm stage schedules retry when wait budget is zero", func(t *testing.T) {
		t.Parallel()

		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			switch r.URL.Path {
			case "/api/v1/admin/accounts/account-busy/test":
				w.Header().Set("Content-Type", "text/event-stream")
				w.WriteHeader(http.StatusOK)
				_, _ = w.Write([]byte("data: {\"type\":\"test_complete\",\"ok\":false,\"status\":\"failed\",\"message\":\"No available accounts\"}\n\n"))
			default:
				http.NotFound(w, r)
			}
		}))
		defer server.Close()

		client, err := newSub2APIClient(server.URL, CreateHostAuth{Type: "bearer", Token: "gateway-key"})
		if err != nil {
			t.Fatalf("newSub2APIClient() error = %v", err)
		}

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

		hostPK := createAppHostRecord(t, store, server.URL)
		packPK := createAppPackRecord(t, store)
		providerPK := createAppProviderRecord(t, store, packPK)
		legacyBatchID := createAppBatchRecord(t, store, hostPK, packPK, providerPK)
		if _, err := store.ManagedResources().Create(context.Background(), sqlite.ManagedResource{
			BatchID:        legacyBatchID,
			HostID:         hostPK,
			ResourceType:   "account",
			HostResourceID: "account-busy",
			ResourceName:   "account-busy",
		}); err != nil {
			t.Fatalf("ManagedResources().Create(account) error = %v", err)
		}

		mustCreateAppImportRun(t, store, sqlite.ImportRun{
			RunID:      "run-confirm-1",
			HostID:     "host-1",
			Mode:       "partial",
			AccessMode: "self_service",
			State:      "running",
			TotalItems: 1,
		})
		mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
			ItemID:                "item-confirm-1",
			RunID:                 "run-confirm-1",
			BaseURL:               server.URL,
			ProviderID:            "deepseek",
			APIKeyFingerprint:     "sha256:confirm",
			ResolvedSmokeModel:    "deepseek-v4-pro",
			CurrentStage:          "confirm",
			ConfirmationStatus:    "pending",
			AccessStatus:          "unknown",
			MatchedAccountState:   "active",
			AccountResolution:     "created",
			LegacyBatchID:         &legacyBatchID,
			CapabilityProfileJSON: `{"transport_profile":{"known_advisories":[]}}`,
		})

		runner := batchImportRuntimeRunner{
			store:      store,
			hostClient: client,
			request: CreateBatchImportRunRequest{
				AccessMode:  provision.AccessModeSelfService,
				ProbeAPIKey: "gateway-key",
			},
		}
		if err := runner.driveRun(context.Background(), "run-confirm-1", 0); err != nil {
			t.Fatalf("driveRun(confirm retry) error = %v", err)
		}

		run, err := store.ImportRuns().GetByRunID(context.Background(), "run-confirm-1")
		if err != nil {
			t.Fatalf("ImportRuns().GetByRunID() error = %v", err)
		}
		if run.State != "running" || run.CompletedItems != 0 {
			t.Fatalf("run = %+v, want still running with no completed items", run)
		}

		item, err := store.ImportRunItems().GetByItemID(context.Background(), "item-confirm-1")
		if err != nil {
			t.Fatalf("ImportRunItems().GetByItemID() error = %v", err)
		}
		if item.CurrentStage != "confirm" || item.RetryCount != 1 || strings.TrimSpace(item.NextRetryAt) == "" {
			t.Fatalf("item = %+v, want confirm stage with scheduled retry", item)
		}
	})
}

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

	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":
			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()

	client, err := newSub2APIClient(server.URL, CreateHostAuth{Type: "bearer", Token: "admin-token"})
	if err != nil {
		t.Fatalf("newSub2APIClient() error = %v", err)
	}

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

	hostPK := createAppHostRecord(t, store, server.URL)
	packPK := createAppPackRecord(t, store)
	providerPK := createAppProviderRecord(t, store, packPK)
	batchID := createAppBatchRecord(t, store, hostPK, packPK, providerPK)
	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)
	}

	runner := batchImportRuntimeRunner{
		store:      store,
		hostClient: client,
		request: CreateBatchImportRunRequest{
			AccessMode:        provision.AccessModeSubscription,
			SubscriptionUsers: []string{"crm-user-1"},
			SubscriptionDays:  30,
		},
	}

	apiKey, err := runner.resolveValidationAPIKey(context.Background(), sqlite.ImportRunItem{
		LegacyBatchID: &batchID,
	})
	if err != nil {
		t.Fatalf("resolveValidationAPIKey(subscription) error = %v", err)
	}
	if !strings.HasPrefix(apiKey, "sk-relay-") {
		t.Fatalf("resolveValidationAPIKey(subscription) = %q, want managed sk-relay-* key", apiKey)
	}
}

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

	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":
			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()

	client, err := newSub2APIClient(server.URL, CreateHostAuth{Type: "bearer", Token: "admin-token"})
	if err != nil {
		t.Fatalf("newSub2APIClient() error = %v", err)
	}

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

	hostPK := createAppHostRecord(t, store, server.URL)
	packPK := createAppPackRecord(t, store)
	providerPK := createAppProviderRecord(t, store, packPK)
	batchID := createAppBatchRecord(t, store, hostPK, packPK, providerPK)

	runner := batchImportRuntimeRunner{
		store:      store,
		hostClient: client,
		request: CreateBatchImportRunRequest{
			AccessMode:        provision.AccessModeSubscription,
			SubscriptionUsers: []string{"crm-user-1"},
			SubscriptionDays:  30,
		},
	}

	_, err = runner.resolveValidationAPIKey(context.Background(), sqlite.ImportRunItem{
		LegacyBatchID: &batchID,
	})
	if err == nil || err.Error() != fmt.Sprintf("%s resource not found for batch %d", "group", batchID) {
		t.Fatalf("resolveValidationAPIKey(subscription missing group) error = %v, want missing group", err)
	}
}

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

	if got := createHostAuthFromLegacyFields(" api-key ", " bearer-token "); got.Type != "bearer" || got.Token != "bearer-token" {
		t.Fatalf("createHostAuthFromLegacyFields() = %+v, want bearer token", got)
	}
	if got := createHostAuthFromLegacyFields(" api-key ", ""); got.Type != "apikey" || got.Token != "api-key" {
		t.Fatalf("createHostAuthFromLegacyFields() = %+v, want apikey", got)
	}
}

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

	hostInfo := hostRecordToInfo(sqlite.Host{
		HostID:              "host-1",
		BaseURL:             "https://sub2api.example.com",
		HostVersion:         "0.1.126",
		AuthType:            "apikey",
		CapabilityProbeJSON: `{"groups":true,"channels":true,"plans":true,"accounts":true,"account_test":true,"account_models":true,"subscriptions":true}`,
	})
	if hostInfo.Status != "supported" || hostInfo.Capabilities == nil || !hostInfo.Capabilities.Subscriptions {
		t.Fatalf("hostRecordToInfo() = %+v, want supported capabilities", hostInfo)
	}

	hostInfo = hostRecordToInfo(sqlite.Host{
		HostID:              "host-2",
		BaseURL:             "https://bad.example.com",
		CapabilityProbeJSON: "{",
	})
	if hostInfo.Capabilities != nil || hostInfo.Status != "" {
		t.Fatalf("hostRecordToInfo(invalid json) = %+v, want nil capabilities and empty status", hostInfo)
	}

	packInfo := packRecordToInfo(sqlite.Pack{
		PackID:         "openai-cn-pack",
		Version:        "1.0.0",
		Vendor:         "OpenAI CN",
		TargetHost:     "sub2api",
		MinHostVersion: "0.1.126",
		MaxHostVersion: "0.2.x",
	})
	if packInfo.PackID != "openai-cn-pack" || packInfo.TargetHost != "sub2api" {
		t.Fatalf("packRecordToInfo() = %+v, want projected pack info", packInfo)
	}
}

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

	if got := deriveAccessStatus(sub2api.GatewayAccessResult{OK: true, HasExpectedModel: true, CompletionOK: true}); got != provision.AccessStatusSubscriptionReady {
		t.Fatalf("deriveAccessStatus(ready) = %q, want %q", got, provision.AccessStatusSubscriptionReady)
	}
	if got := deriveAccessStatus(sub2api.GatewayAccessResult{OK: true, HasExpectedModel: true, CompletionOK: false}); got != provision.AccessStatusBroken {
		t.Fatalf("deriveAccessStatus(broken) = %q, want %q", got, provision.AccessStatusBroken)
	}
	if got := defaultPositiveInt(3, 9); got != 3 {
		t.Fatalf("defaultPositiveInt(3, 9) = %d, want 3", got)
	}
	if got := defaultPositiveInt(0, 9); got != 9 {
		t.Fatalf("defaultPositiveInt(0, 9) = %d, want 9", got)
	}
}

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

	view := batch.ItemSummaryProjection{
		ItemID:              "item-1",
		BaseURL:             "https://kimi.example.com/v1",
		ProviderID:          "kimi-a7m",
		CurrentStage:        "done",
		ConfirmationStatus:  "advisory",
		AccessStatus:        "active",
		MatchedAccountState: "active",
		AccountResolution:   "reused",
		AdvisoryMessages:    []string{"warning"},
	}
	hasWarning := true
	if !matchesItemFilters(view, ListBatchImportRunItemsRequest{
		CurrentStage:        "done",
		ConfirmationStatus:  "advisory",
		AccessStatus:        "active",
		ProviderID:          "kimi-a7m",
		MatchedAccountState: "active",
		AccountResolution:   "reused",
		Query:               "kimi",
		HasWarning:          &hasWarning,
	}) {
		t.Fatal("matchesItemFilters() = false, want match")
	}
	noWarning := false
	if matchesItemFilters(view, ListBatchImportRunItemsRequest{HasWarning: &noWarning}) {
		t.Fatal("matchesItemFilters(has_warning=false) = true, want false")
	}
	if matchesItemFilters(view, ListBatchImportRunItemsRequest{Query: "other"}) {
		t.Fatal("matchesItemFilters(query=other) = true, want false")
	}
}

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

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

	mustCreateAppImportRun(t, store, sqlite.ImportRun{
		RunID:      "run-1",
		HostID:     "host-1",
		Mode:       "partial",
		AccessMode: "self_service",
		State:      "running",
	})
	mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:00' WHERE run_id = 'run-1'`)
	mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
		ItemID:               "item-1",
		RunID:                "run-1",
		BaseURL:              "https://kimi.example.com/v1",
		ProviderID:           "kimi-a7m",
		APIKeyFingerprint:    "sha256:1",
		CurrentStage:         "done",
		ConfirmationStatus:   "confirmed",
		AccessStatus:         "active",
		MatchedAccountState:  "active",
		AccountResolution:    "reused",
		AdvisoryMessagesJSON: `["gateway_warmup_retry_succeeded"]`,
	})

	action := buildGetBatchImportRunAction(appTestDSN(t, store))
	run, err := action(context.Background(), "run-1")
	if err != nil {
		t.Fatalf("buildGetBatchImportRunAction() error = %v", err)
	}
	if run.RunID != "run-1" {
		t.Fatalf("run.RunID = %q, want run-1", run.RunID)
	}
	if _, err := action(context.Background(), "missing"); err == nil || err.Error() != "run not found: missing" {
		t.Fatalf("missing run error = %v, want run not found", err)
	}

	itemAction := buildGetBatchImportRunItemAction(appTestDSN(t, store))
	item, err := itemAction(context.Background(), GetBatchImportRunItemRequest{RunID: "run-1", ItemID: "item-1"})
	if err != nil {
		t.Fatalf("buildGetBatchImportRunItemAction() error = %v", err)
	}
	if item.ItemID != "item-1" || item.AccountResolution != "reused" {
		t.Fatalf("item = %+v, want projected item", item)
	}
	if _, err := itemAction(context.Background(), GetBatchImportRunItemRequest{RunID: "run-2", ItemID: "item-1"}); err == nil || err.Error() != "item not found in run run-2" {
		t.Fatalf("wrong run error = %v, want item not found in run", err)
	}
}

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

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

	host1, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:      "host-1",
		BaseURL:     "https://one.example.com",
		HostVersion: "0.1.126",
		AuthToken:   "token-1",
	})
	if err != nil {
		t.Fatalf("Hosts().Create(host1) error = %v", err)
	}
	host2, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:      "host-2",
		BaseURL:     "https://two.example.com",
		HostVersion: "0.1.126",
		AuthToken:   "token-2",
	})
	if err != nil {
		t.Fatalf("Hosts().Create(host2) error = %v", err)
	}
	packID, err := store.Packs().Create(context.Background(), sqlite.Pack{
		PackID:   "openai-cn-pack",
		Version:  "1.0.0",
		Checksum: "checksum-1",
	})
	if err != nil {
		t.Fatalf("Packs().Create() error = %v", err)
	}
	providerID, err := store.Providers().Create(context.Background(), sqlite.Provider{
		PackID:      packID,
		ProviderID:  "deepseek",
		DisplayName: "DeepSeek",
		BaseURL:     "https://api.example.com",
		Platform:    "openai",
	})
	if err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}
	batch1, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       host1,
		PackID:       packID,
		ProviderID:   providerID,
		Mode:         provision.ImportModePartial,
		BatchStatus:  provision.BatchStatusSucceeded,
		AccessStatus: provision.AccessStatusSelfServiceReady,
	})
	if err != nil {
		t.Fatalf("ImportBatches().Create(batch1) error = %v", err)
	}
	batch2, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       host2,
		PackID:       packID,
		ProviderID:   providerID,
		Mode:         provision.ImportModeStrict,
		BatchStatus:  provision.BatchStatusSucceeded,
		AccessStatus: provision.AccessStatusSubscriptionReady,
	})
	if err != nil {
		t.Fatalf("ImportBatches().Create(batch2) error = %v", err)
	}
	if _, err := store.AccessClosures().Create(context.Background(), sqlite.AccessClosureRecord{
		BatchID:     batch1,
		ClosureType: provision.AccessModeSelfService,
		Status:      provision.AccessStatusSelfServiceReady,
	}); err != nil {
		t.Fatalf("AccessClosures().Create(batch1) error = %v", err)
	}
	if _, err := store.AccessClosures().Create(context.Background(), sqlite.AccessClosureRecord{
		BatchID:     batch2,
		ClosureType: provision.AccessModeSubscription,
		Status:      provision.AccessStatusSubscriptionReady,
	}); err != nil {
		t.Fatalf("AccessClosures().Create(batch2) error = %v", err)
	}

	if _, err := resolveProvidersForQuery(context.Background(), nil, ProviderQueryRequest{}); err == nil || err.Error() != "store is required" {
		t.Fatalf("resolveProvidersForQuery(nil store) error = %v, want store is required", err)
	}
	if _, err := resolveProvidersForQuery(context.Background(), store, ProviderQueryRequest{}); err == nil || err.Error() != "provider_id is required" {
		t.Fatalf("resolveProvidersForQuery(missing provider) error = %v, want provider_id is required", err)
	}
	providers, err := resolveProvidersForQuery(context.Background(), store, ProviderQueryRequest{ProviderID: "deepseek", PackID: "openai-cn-pack"})
	if err != nil {
		t.Fatalf("resolveProvidersForQuery() error = %v", err)
	}
	if len(providers) != 1 || providers[0].ProviderID != "deepseek" {
		t.Fatalf("resolveProvidersForQuery() = %+v, want deepseek provider", providers)
	}

	if _, err := resolveLatestAccessStatus(context.Background(), nil, sqlite.Provider{}, ""); err == nil || err.Error() != "store is required" {
		t.Fatalf("resolveLatestAccessStatus(nil store) error = %v, want store is required", err)
	}
	status, err := resolveLatestAccessStatus(context.Background(), store, providers[0], "host-1")
	if err != nil {
		t.Fatalf("resolveLatestAccessStatus(host-1) error = %v", err)
	}
	if status != provision.AccessStatusSelfServiceReady {
		t.Fatalf("resolveLatestAccessStatus(host-1) = %q, want %q", status, provision.AccessStatusSelfServiceReady)
	}
	if _, err := resolveLatestAccessStatus(context.Background(), store, providers[0], ""); err == nil || err.Error() != "provider exists on multiple hosts; host_id is required" {
		t.Fatalf("resolveLatestAccessStatus(multi-host) error = %v, want multi-host error", err)
	}
}

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

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

	mustCreateAppImportRun(t, store, sqlite.ImportRun{
		RunID:         "run-1",
		HostID:        "host-1",
		Mode:          "partial",
		AccessMode:    "self_service",
		State:         "running",
		TotalItems:    1,
		WarningItems:  1,
		ActiveItems:   1,
		BrokenItems:   0,
		DegradedItems: 0,
	})
	mustCreateAppImportRun(t, store, sqlite.ImportRun{
		RunID:      "run-2",
		HostID:     "host-1",
		Mode:       "strict",
		AccessMode: "subscription",
		State:      "completed",
	})
	mustCreateAppImportRun(t, store, sqlite.ImportRun{
		RunID:      "run-3",
		HostID:     "host-1",
		Mode:       "strict",
		AccessMode: "subscription",
		State:      "completed",
	})
	mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:03' WHERE run_id = 'run-1'`)
	mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:02' WHERE run_id = 'run-2'`)
	mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:01' WHERE run_id = 'run-3'`)

	mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
		ItemID:               "item-1",
		RunID:                "run-1",
		BaseURL:              "https://kimi.example.com/v1",
		ProviderID:           "kimi-a7m",
		APIKeyFingerprint:    "sha256:1",
		CurrentStage:         "done",
		ConfirmationStatus:   "advisory",
		AccessStatus:         "active",
		MatchedAccountState:  "active",
		AccountResolution:    "reused",
		AdvisoryMessagesJSON: `["warning"]`,
	})
	mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
		ItemID:               "item-2",
		RunID:                "run-2",
		BaseURL:              "https://other.example.com/v1",
		ProviderID:           "deepseek",
		APIKeyFingerprint:    "sha256:2",
		CurrentStage:         "confirm",
		ConfirmationStatus:   "pending",
		AccessStatus:         "broken",
		MatchedAccountState:  "broken",
		AccountResolution:    "created",
		AdvisoryMessagesJSON: `[]`,
	})
	mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
		ItemID:               "item-3",
		RunID:                "run-3",
		BaseURL:              "https://other.example.com/v1",
		ProviderID:           "deepseek-backup",
		APIKeyFingerprint:    "sha256:3",
		CurrentStage:         "confirm",
		ConfirmationStatus:   "pending",
		AccessStatus:         "active",
		MatchedAccountState:  "active",
		AccountResolution:    "reused",
		AdvisoryMessagesJSON: `[]`,
	})

	listRuns := buildListBatchImportRunsAction(appTestDSN(t, store))
	runs, err := listRuns(context.Background(), ListBatchImportRunsRequest{
		State: "completed",
		Query: "other.example.com",
		Limit: 1,
	})
	if err != nil {
		t.Fatalf("buildListBatchImportRunsAction() error = %v", err)
	}
	if len(runs.Runs) != 1 || runs.Runs[0].RunID != "run-2" {
		t.Fatalf("runs = %+v, want [run-2]", runs.Runs)
	}
	if runs.NextCursor == nil || *runs.NextCursor != "run-3" {
		t.Fatalf("runs.NextCursor = %v, want run-3", runs.NextCursor)
	}

	listItems := buildListBatchImportRunItemsAction(appTestDSN(t, store))
	hasWarning := true
	items, err := listItems(context.Background(), ListBatchImportRunItemsRequest{
		RunID:      "run-1",
		HasWarning: &hasWarning,
		Query:      "kimi",
		Limit:      10,
	})
	if err != nil {
		t.Fatalf("buildListBatchImportRunItemsAction() error = %v", err)
	}
	if len(items.Items) != 1 || items.Items[0].ItemID != "item-1" {
		t.Fatalf("items = %+v, want [item-1]", items.Items)
	}
	if items.NextCursor != nil {
		t.Fatalf("items.NextCursor = %v, want nil", items.NextCursor)
	}
	if _, err := listItems(context.Background(), ListBatchImportRunItemsRequest{RunID: "missing"}); err == nil || err.Error() != "run not found: missing" {
		t.Fatalf("missing items run error = %v, want run not found", err)
	}
}

func appTestDSN(t *testing.T, store *sqlite.DB) string {
	t.Helper()

	row := store.SQLDB().QueryRow(`PRAGMA database_list`)
	var seq int
	var name string
	var file string
	if err := row.Scan(&seq, &name, &file); err != nil {
		t.Fatalf("PRAGMA database_list scan error = %v", err)
	}
	return "file:" + file + "?_busy_timeout=5000&_pragma=foreign_keys(0)"
}

func mustCreateAppImportRun(t *testing.T, store *sqlite.DB, run sqlite.ImportRun) {
	t.Helper()
	if err := store.ImportRuns().Create(context.Background(), run); err != nil {
		t.Fatalf("ImportRuns().Create() error = %v", err)
	}
}

func mustCreateAppImportRunItem(t *testing.T, store *sqlite.DB, item sqlite.ImportRunItem) {
	t.Helper()
	if err := store.ImportRunItems().Create(context.Background(), item); err != nil {
		t.Fatalf("ImportRunItems().Create() error = %v", err)
	}
}

func mustExecSQL(t *testing.T, store *sqlite.DB, query string, args ...any) {
	t.Helper()
	if _, err := store.SQLDB().Exec(query, args...); err != nil {
		t.Fatalf("Exec(%q) error = %v", query, err)
	}
}

func createAppHostRecord(t *testing.T, store *sqlite.DB, baseURL string) int64 {
	t.Helper()
	id, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:      "host-" + sanitizeAppTestName(t.Name()),
		BaseURL:     baseURL,
		HostVersion: "0.1.126",
		AuthType:    "apikey",
		AuthToken:   "host-token",
	})
	if err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	return id
}

func createAppPackRecord(t *testing.T, store *sqlite.DB) int64 {
	t.Helper()
	id, err := store.Packs().Create(context.Background(), sqlite.Pack{
		PackID:   "pack-" + sanitizeAppTestName(t.Name()),
		Version:  "1.0.0",
		Checksum: "checksum-" + sanitizeAppTestName(t.Name()),
	})
	if err != nil {
		t.Fatalf("Packs().Create() error = %v", err)
	}
	return id
}

func createAppProviderRecord(t *testing.T, store *sqlite.DB, packID int64) int64 {
	t.Helper()
	id, err := store.Providers().Create(context.Background(), sqlite.Provider{
		PackID:      packID,
		ProviderID:  "provider-" + sanitizeAppTestName(t.Name()),
		DisplayName: "Provider",
		BaseURL:     "https://provider.example.com",
		Platform:    "openai",
	})
	if err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}
	return id
}

func createAppBatchRecord(t *testing.T, store *sqlite.DB, hostID, packID, providerID int64) int64 {
	t.Helper()
	id, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       hostID,
		PackID:       packID,
		ProviderID:   providerID,
		Mode:         provision.ImportModePartial,
		BatchStatus:  provision.BatchStatusSucceeded,
		AccessStatus: provision.AccessStatusSelfServiceReady,
	})
	if err != nil {
		t.Fatalf("ImportBatches().Create() error = %v", err)
	}
	return id
}

func sanitizeAppTestName(name string) string {
	var b strings.Builder
	for _, c := range name {
		switch {
		case c >= 'a' && c <= 'z', c >= '0' && c <= '9':
			b.WriteRune(c)
		case c >= 'A' && c <= 'Z':
			b.WriteRune(c + ('a' - 'A'))
		}
	}
	if b.Len() == 0 {
		return "default"
	}
	return b.String()
}

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

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

	if _, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:      "host-1",
		BaseURL:     "https://sub2api.example.com",
		HostVersion: "0.1.126",
		AuthType:    "",
		AuthToken:   "host-token",
	}); err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}

	hostRow, client, err := resolveManagedHost(context.Background(), store, "host-1", "", CreateHostAuth{})
	if err != nil {
		t.Fatalf("resolveManagedHost() error = %v", err)
	}
	if hostRow.HostID != "host-1" || client == nil {
		t.Fatalf("resolveManagedHost() = (%+v, %v), want host-1 and client", hostRow, client)
	}
	if _, _, err := resolveManagedHost(context.Background(), store, "host-1", "https://other.example.com", CreateHostAuth{}); err == nil || !strings.Contains(err.Error(), `host "host-1" base_url mismatch`) {
		t.Fatalf("resolveManagedHost(mismatch) error = %v, want mismatch", err)
	}
	if _, _, err := resolveManagedHost(context.Background(), store, "", "", CreateHostAuth{}); err == nil || err.Error() != "host_id is required" {
		t.Fatalf("resolveManagedHost(empty) error = %v, want host_id is required", err)
	}

	if auth := authFromStoredHost(sqlite.Host{AuthType: "", AuthToken: " token "}); auth.Type != "apikey" || auth.Token != "token" {
		t.Fatalf("authFromStoredHost(default) = %+v, want apikey/token", auth)
	}
	if _, err := newSub2APIClient("https://sub2api.example.com", CreateHostAuth{Type: "other", Token: "t"}); err == nil || !strings.Contains(err.Error(), `unsupported auth type "other"`) {
		t.Fatalf("newSub2APIClient(unsupported) error = %v, want unsupported auth type", err)
	}
	if _, err := newSub2APIClient("https://sub2api.example.com", CreateHostAuth{Type: "apikey"}); err == nil || !strings.Contains(err.Error(), "auth.token is required") {
		t.Fatalf("newSub2APIClient(missing token) error = %v, want auth.token is required", err)
	}
}

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

	t.Run("handleAssignAccessSubscriptions returns success", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "POST", "/providers/deepseek/access/subscriptions", map[string]any{
			"host_id":            "host-1",
			"access_api_key":     "user-key",
			"subscription_users": []string{"user-1"},
		}, "")
		req.SetPathValue("providerID", "deepseek")
		rec := &responseRecorder{header: map[string][]string{}}
		handleAssignAccessSubscriptions(rec, req, func(_ context.Context, got AssignAccessSubscriptionsRequest) (AssignAccessSubscriptionsResult, error) {
			if got.ProviderID != "deepseek" || got.HostID != "host-1" || got.AccessAPIKey != "user-key" || len(got.SubscriptionUsers) != 1 || got.SubscriptionUsers[0] != "user-1" {
				t.Fatalf("AssignAccessSubscriptionsRequest = %+v, want projected request", got)
			}
			return AssignAccessSubscriptionsResult{Assigned: 1}, nil
		})
		assertStatusCode(t, rec, 200)
		assertJSONContains(t, rec.Body().Bytes(), "assigned", float64(1))
	})

	t.Run("handleAccessPreview falls back to query values", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "POST", "/providers/deepseek/access/preview?pack_id=openai-cn-pack&host_id=host-1", map[string]any{
			"mode": "self_service",
		}, "")
		req.SetPathValue("providerID", "deepseek")
		rec := &responseRecorder{header: map[string][]string{}}
		handleAccessPreview(rec, req, func(_ context.Context, got AccessPreviewRequest) (AccessPreviewResult, error) {
			if got.ProviderID != "deepseek" || got.PackID != "openai-cn-pack" || got.HostID != "host-1" {
				t.Fatalf("AccessPreviewRequest = %+v, want query fallback values", got)
			}
			return AccessPreviewResult{Available: true, Mode: got.Mode}, nil
		})
		assertStatusCode(t, rec, 200)
		assertJSONContains(t, rec.Body().Bytes(), "available", true)
		assertJSONContains(t, rec.Body().Bytes(), "mode", "self_service")
	})
}

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

	t.Run("handleListProviderImportBatches returns query-scoped payload", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "GET", "/providers/deepseek/import-batches?pack_id=openai-cn-pack&host_id=host-1", map[string]any{}, "")
		req.SetPathValue("providerID", "deepseek")
		rec := &responseRecorder{header: map[string][]string{}}
		handleListProviderImportBatches(rec, req, func(_ context.Context, got ProviderQueryRequest) ([]ImportBatchInfo, error) {
			if got.ProviderID != "deepseek" || got.PackID != "openai-cn-pack" || got.HostID != "host-1" {
				t.Fatalf("ProviderQueryRequest = %+v, want provider/pack/host filters", got)
			}
			return nil, nil
		})
		assertStatusCode(t, rec, 200)
		batches, ok := decodeTopLevelArray(t, rec.Body().Bytes(), "batches")
		if !ok || len(batches) != 0 {
			t.Fatalf("batches = %#v, want empty array", batches)
		}
	})

	t.Run("handleRollbackBatch validates batch id", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "POST", "/import-batches/bad/rollback", map[string]any{}, "")
		req.SetPathValue("batchID", "bad")
		rec := &responseRecorder{header: map[string][]string{}}
		handleRollbackBatch(rec, req, func(context.Context, RollbackBatchRequest) (provision.RollbackReport, error) {
			t.Fatal("rollback fn should not be called for invalid batch id")
			return provision.RollbackReport{}, nil
		})
		assertStatusCode(t, rec, 400)
		assertJSONContains(t, rec.Body().Bytes(), "error.message", "batch_id must be a positive integer")
	})

	t.Run("handleRollbackBatch returns summary", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "POST", "/import-batches/17/rollback", map[string]any{}, "")
		req.SetPathValue("batchID", "17")
		rec := &responseRecorder{header: map[string][]string{}}
		handleRollbackBatch(rec, req, func(_ context.Context, got RollbackBatchRequest) (provision.RollbackReport, error) {
			if got.BatchID != 17 {
				t.Fatalf("RollbackBatchRequest.BatchID = %d, want 17", got.BatchID)
			}
			return provision.RollbackReport{AccountsDeleted: 2, PlansDeleted: 1, ChannelsDeleted: 1, GroupsDeleted: 1}, nil
		})
		assertStatusCode(t, rec, 200)
		assertJSONContains(t, rec.Body().Bytes(), "batch_id", float64(17))
		assertJSONContains(t, rec.Body().Bytes(), "deleted_accounts", float64(2))
	})

	t.Run("handleCreateHost returns payload", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "POST", "/hosts", map[string]any{
			"name":     "host-1",
			"base_url": "https://sub2api.example.com",
			"auth":     map[string]any{"type": "apikey", "token": "host-token"},
		}, "")
		rec := &responseRecorder{header: map[string][]string{}}
		handleCreateHost(rec, req, func(_ context.Context, got CreateHostRequest) (HostInfo, error) {
			if got.Name != "host-1" || got.BaseURL != "https://sub2api.example.com" || got.Auth.Token != "host-token" {
				t.Fatalf("CreateHostRequest = %+v, want decoded request", got)
			}
			return HostInfo{HostID: got.Name, BaseURL: got.BaseURL, HostVersion: "0.1.126", AuthType: "apikey"}, nil
		})
		assertStatusCode(t, rec, 200)
		assertJSONContains(t, rec.Body().Bytes(), "host_id", "host-1")
		assertJSONContains(t, rec.Body().Bytes(), "base_url", "https://sub2api.example.com")
	})
}

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

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

	packID := createAppPackRecord(t, store)
	if _, err := store.Providers().Create(context.Background(), sqlite.Provider{
		PackID:      packID,
		ProviderID:  "provider-a",
		DisplayName: "Provider A",
		BaseURL:     "https://provider-a.example.com",
		Platform:    "openai",
	}); err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}

	actions := NewActionSet(appTestDSN(t, store))

	packs, err := actions.ListPacks(context.Background())
	if err != nil {
		t.Fatalf("ListPacks() error = %v", err)
	}
	if len(packs) != 1 || packs[0].PackID == "" {
		t.Fatalf("ListPacks() = %+v, want single persisted pack", packs)
	}

	packInfo, err := actions.GetPack(context.Background(), packs[0].PackID)
	if err != nil {
		t.Fatalf("GetPack() error = %v", err)
	}
	if packInfo.PackID != packs[0].PackID {
		t.Fatalf("GetPack() = %+v, want pack_id %q", packInfo, packs[0].PackID)
	}

	providers, err := actions.ListPackProviders(context.Background(), packs[0].PackID)
	if err != nil {
		t.Fatalf("ListPackProviders() error = %v", err)
	}
	if len(providers) != 1 || providers[0].ProviderID != "provider-a" {
		t.Fatalf("ListPackProviders() = %+v, want provider-a", providers)
	}
}

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

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

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

	actions := NewActionSet(appTestDSN(t, store))
	host, err := actions.CreateHost(context.Background(), CreateHostRequest{
		Name:    "prod-sub2api",
		BaseURL: server.URL,
		Auth:    CreateHostAuth{Type: "apikey", Token: "host-token"},
	})
	if err != nil {
		t.Fatalf("CreateHost() error = %v", err)
	}
	if host.HostID != "prod-sub2api" || host.HostVersion != "0.1.126" || host.Status != "supported" {
		t.Fatalf("CreateHost() = %+v, want stored supported host", host)
	}

	stored, err := store.Hosts().GetByHostID(context.Background(), "prod-sub2api")
	if err != nil {
		t.Fatalf("Hosts().GetByHostID() error = %v", err)
	}
	if stored.BaseURL != server.URL || stored.AuthToken != "host-token" {
		t.Fatalf("stored host = %+v, want persisted connection details", stored)
	}
}

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

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

	hostA, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:      "host-a",
		BaseURL:     "https://host-a.example.com",
		HostVersion: "0.1.126",
		AuthToken:   "token-a",
	})
	if err != nil {
		t.Fatalf("Hosts().Create(host-a) error = %v", err)
	}
	hostB, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:      "host-b",
		BaseURL:     "https://host-b.example.com",
		HostVersion: "0.1.126",
		AuthToken:   "token-b",
	})
	if err != nil {
		t.Fatalf("Hosts().Create(host-b) error = %v", err)
	}
	packID := createAppPackRecord(t, store)
	providerID, err := store.Providers().Create(context.Background(), sqlite.Provider{
		PackID:      packID,
		ProviderID:  "shared-provider",
		DisplayName: "Shared Provider",
		BaseURL:     "https://provider.example.com",
		Platform:    "openai",
	})
	if err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}
	if _, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       hostA,
		PackID:       packID,
		ProviderID:   providerID,
		Mode:         provision.ImportModePartial,
		BatchStatus:  provision.BatchStatusSucceeded,
		AccessStatus: provision.AccessStatusSelfServiceReady,
	}); err != nil {
		t.Fatalf("ImportBatches().Create(host-a) error = %v", err)
	}
	if _, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       hostB,
		PackID:       packID,
		ProviderID:   providerID,
		Mode:         provision.ImportModeStrict,
		BatchStatus:  provision.BatchStatusPartial,
		AccessStatus: provision.AccessStatusBroken,
	}); err != nil {
		t.Fatalf("ImportBatches().Create(host-b) error = %v", err)
	}

	actions := NewActionSet(appTestDSN(t, store))
	if _, err := actions.ListProviderImportBatches(context.Background(), ProviderQueryRequest{ProviderID: "shared-provider"}); err == nil || err.Error() != "provider exists on multiple hosts; host_id is required" {
		t.Fatalf("ListProviderImportBatches(multi-host) error = %v, want host_id required", err)
	}

	batches, err := actions.ListProviderImportBatches(context.Background(), ProviderQueryRequest{
		ProviderID: "shared-provider",
		HostID:     "host-a",
	})
	if err != nil {
		t.Fatalf("ListProviderImportBatches(host filter) error = %v", err)
	}
	if len(batches) != 1 || batches[0].BatchStatus != provision.BatchStatusSucceeded {
		t.Fatalf("ListProviderImportBatches(host filter) = %+v, want single succeeded batch", batches)
	}
}

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

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

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

	hostID, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:              "host-main",
		BaseURL:             server.URL,
		HostVersion:         "0.0.1",
		CapabilityProbeJSON: "{}",
		AuthType:            "apikey",
		AuthToken:           "host-token",
	})
	if err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	packID := createAppPackRecord(t, store)
	providerID, err := store.Providers().Create(context.Background(), sqlite.Provider{
		PackID:      packID,
		ProviderID:  "preview-provider",
		DisplayName: "Preview Provider",
		BaseURL:     "https://provider.example.com",
		Platform:    "openai",
	})
	if err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}
	batchID, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       hostID,
		PackID:       packID,
		ProviderID:   providerID,
		Mode:         provision.ImportModePartial,
		BatchStatus:  provision.BatchStatusSucceeded,
		AccessStatus: provision.AccessStatusSelfServiceReady,
	})
	if err != nil {
		t.Fatalf("ImportBatches().Create() error = %v", err)
	}
	if _, err := store.AccessClosures().Create(context.Background(), sqlite.AccessClosureRecord{
		BatchID:     batchID,
		ClosureType: provision.AccessModeSelfService,
		Status:      provision.AccessStatusSelfServiceReady,
		DetailsJSON: `{}`,
	}); err != nil {
		t.Fatalf("AccessClosures().Create() error = %v", err)
	}

	actions := NewActionSet(appTestDSN(t, store))

	hosts, err := actions.ListHosts(context.Background())
	if err != nil {
		t.Fatalf("ListHosts() error = %v", err)
	}
	if len(hosts) != 1 || hosts[0].HostID != "host-main" {
		t.Fatalf("ListHosts() = %+v, want host-main", hosts)
	}

	hostInfo, err := actions.GetHost(context.Background(), "host-main")
	if err != nil {
		t.Fatalf("GetHost() error = %v", err)
	}
	if hostInfo.HostID != "host-main" || hostInfo.BaseURL != server.URL {
		t.Fatalf("GetHost() = %+v, want stored host", hostInfo)
	}

	probed, err := actions.ProbeHost(context.Background(), ProbeHostRequest{HostID: "host-main"})
	if err != nil {
		t.Fatalf("ProbeHost() error = %v", err)
	}
	if probed.Status != "supported" || probed.HostVersion != "0.1.126" {
		t.Fatalf("ProbeHost() = %+v, want supported host 0.1.126", probed)
	}

	packRow, err := store.Packs().GetByID(context.Background(), packID)
	if err != nil {
		t.Fatalf("Packs().GetByID() error = %v", err)
	}
	preview, err := actions.AccessPreview(context.Background(), AccessPreviewRequest{
		ProviderID: "preview-provider",
		PackID:     packRow.PackID,
		HostID:     "host-main",
		Mode:       provision.AccessModeSelfService,
	})
	if err != nil {
		t.Fatalf("AccessPreview(self_service) error = %v", err)
	}
	if !preview.Available {
		t.Fatalf("AccessPreview(self_service) = %+v, want available=true", preview)
	}

	preview, err = actions.AccessPreview(context.Background(), AccessPreviewRequest{
		ProviderID: "preview-provider",
		PackID:     packRow.PackID,
		HostID:     "host-main",
		Mode:       provision.AccessModeSubscription,
	})
	if err != nil {
		t.Fatalf("AccessPreview(subscription) error = %v", err)
	}
	if preview.Available {
		t.Fatalf("AccessPreview(subscription) = %+v, want available=false", preview)
	}

	if err := actions.DeleteHost(context.Background(), "host-main"); err == nil {
		t.Fatal("DeleteHost() error = nil, want host_in_use conflict")
	} else {
		httpErr, ok := err.(*httpError)
		if !ok || httpErr.StatusCode != http.StatusConflict || httpErr.Code != "host_in_use" {
			t.Fatalf("DeleteHost() error = %T %v, want *httpError host_in_use conflict", err, err)
		}
	}
}

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

	t.Run("handleListHosts returns empty array", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "GET", "/hosts", map[string]any{}, "")
		rec := &responseRecorder{header: map[string][]string{}}
		handleListHosts(rec, req, func(context.Context) ([]HostInfo, error) { return nil, nil })
		assertStatusCode(t, rec, 200)
		hosts, ok := decodeTopLevelArray(t, rec.Body().Bytes(), "hosts")
		if !ok || len(hosts) != 0 {
			t.Fatalf("hosts = %#v, want empty array", hosts)
		}
	})

	t.Run("handleGetHost requires host id", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "GET", "/hosts/", map[string]any{}, "")
		rec := &responseRecorder{header: map[string][]string{}}
		handleGetHost(rec, req, func(context.Context, string) (HostInfo, error) { return HostInfo{}, nil })
		assertStatusCode(t, rec, 400)
		assertJSONContains(t, rec.Body().Bytes(), "error.message", "host_id is required")
	})

	t.Run("handleProbeHost returns payload", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "POST", "/hosts/host-1/probe", map[string]any{
			"auth": map[string]any{"type": "apikey", "token": "token"},
		}, "")
		req.SetPathValue("hostID", "host-1")
		rec := &responseRecorder{header: map[string][]string{}}
		handleProbeHost(rec, req, func(_ context.Context, got ProbeHostRequest) (HostInfo, error) {
			if got.HostID != "host-1" || got.Auth.Token != "token" {
				t.Fatalf("ProbeHostRequest = %+v, want host-1/token", got)
			}
			return HostInfo{HostID: "host-1", HostVersion: "0.1.126", Status: "supported"}, nil
		})
		assertStatusCode(t, rec, 200)
		assertJSONContains(t, rec.Body().Bytes(), "host_id", "host-1")
		assertJSONContains(t, rec.Body().Bytes(), "status", "supported")
	})
}

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

	t.Run("handleListPacks returns empty array", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "GET", "/packs", map[string]any{}, "")
		rec := &responseRecorder{header: map[string][]string{}}
		handleListPacks(rec, req, func(context.Context) ([]PackInfo, error) { return nil, nil })
		assertStatusCode(t, rec, 200)
		packs, ok := decodeTopLevelArray(t, rec.Body().Bytes(), "packs")
		if !ok || len(packs) != 0 {
			t.Fatalf("packs = %#v, want empty array", packs)
		}
	})

	t.Run("handleGetPack requires pack id", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "GET", "/packs/", map[string]any{}, "")
		rec := &responseRecorder{header: map[string][]string{}}
		handleGetPack(rec, req, func(context.Context, string) (PackInfo, error) { return PackInfo{}, nil })
		assertStatusCode(t, rec, 400)
		assertJSONContains(t, rec.Body().Bytes(), "error.message", "pack_id is required")
	})

	t.Run("handleGetPack returns payload", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "GET", "/packs/openai-cn-pack", map[string]any{}, "")
		req.SetPathValue("packID", "openai-cn-pack")
		rec := &responseRecorder{header: map[string][]string{}}
		handleGetPack(rec, req, func(_ context.Context, packID string) (PackInfo, error) {
			if packID != "openai-cn-pack" {
				t.Fatalf("packID = %q, want openai-cn-pack", packID)
			}
			return PackInfo{PackID: "openai-cn-pack", Version: "1.0.0"}, nil
		})
		assertStatusCode(t, rec, 200)
		assertJSONContains(t, rec.Body().Bytes(), "pack_id", "openai-cn-pack")
	})

	t.Run("handleListPackProviders returns array", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "GET", "/packs/openai-cn-pack/providers", map[string]any{}, "")
		req.SetPathValue("packID", "openai-cn-pack")
		rec := &responseRecorder{header: map[string][]string{}}
		handleListPackProviders(rec, req, func(_ context.Context, packID string) ([]PackProviderInfo, error) {
			if packID != "openai-cn-pack" {
				t.Fatalf("packID = %q, want openai-cn-pack", packID)
			}
			return nil, nil
		})
		assertStatusCode(t, rec, 200)
		providers, ok := decodeTopLevelArray(t, rec.Body().Bytes(), "providers")
		if !ok || len(providers) != 0 {
			t.Fatalf("providers = %#v, want empty array", providers)
		}
	})

	t.Run("handleCreateHost decodes request", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "POST", "/hosts", map[string]any{
			"name":     "host-1",
			"base_url": "https://sub2api.example.com",
			"auth":     map[string]any{"type": "apikey", "token": "host-token"},
		}, "")
		rec := &responseRecorder{header: map[string][]string{}}
		handleCreateHost(rec, req, func(_ context.Context, req CreateHostRequest) (HostInfo, error) {
			if req.BaseURL != "https://sub2api.example.com" || req.Auth.Token != "host-token" {
				t.Fatalf("request = %+v, want decoded create host request", req)
			}
			return HostInfo{HostID: "host-1", BaseURL: req.BaseURL}, nil
		})
		assertStatusCode(t, rec, 200)
		assertJSONContains(t, rec.Body().Bytes(), "host_id", "host-1")
	})

	t.Run("handleProbeHost requires host id", func(t *testing.T) {
		t.Parallel()
		req := httptestRequest(t, "POST", "/hosts/probe", map[string]any{
			"auth": map[string]any{"type": "apikey", "token": "host-token"},
		}, "")
		rec := &responseRecorder{header: map[string][]string{}}
		handleProbeHost(rec, req, func(context.Context, ProbeHostRequest) (HostInfo, error) { return HostInfo{}, nil })
		assertStatusCode(t, rec, 400)
		assertJSONContains(t, rec.Body().Bytes(), "error.message", "host_id is required")
	})
}

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

	t.Run("list and pack wrappers handle missing actions", func(t *testing.T) {
		t.Parallel()

		cases := []struct {
			name       string
			path       string
			run        func(*responseRecorder, *http.Request)
			wantStatus int
			wantCode   string
		}{
			{
				name: "list packs",
				path: "/packs",
				run: func(rec *responseRecorder, req *http.Request) {
					handleListPacks(rec, req, nil)
				},
				wantStatus: http.StatusInternalServerError,
				wantCode:   "server_misconfigured",
			},
			{
				name: "list provider import batches",
				path: "/providers/deepseek/import-batches",
				run: func(rec *responseRecorder, req *http.Request) {
					req.SetPathValue("providerID", "deepseek")
					handleListProviderImportBatches(rec, req, nil)
				},
				wantStatus: http.StatusInternalServerError,
				wantCode:   "server_misconfigured",
			},
			{
				name: "get pack",
				path: "/packs/openai-cn-pack",
				run: func(rec *responseRecorder, req *http.Request) {
					req.SetPathValue("packID", "openai-cn-pack")
					handleGetPack(rec, req, nil)
				},
				wantStatus: http.StatusInternalServerError,
				wantCode:   "server_misconfigured",
			},
			{
				name: "list pack providers",
				path: "/packs/openai-cn-pack/providers",
				run: func(rec *responseRecorder, req *http.Request) {
					req.SetPathValue("packID", "openai-cn-pack")
					handleListPackProviders(rec, req, nil)
				},
				wantStatus: http.StatusInternalServerError,
				wantCode:   "server_misconfigured",
			},
			{
				name: "rollback batch",
				path: "/import-batches/11/rollback",
				run: func(rec *responseRecorder, req *http.Request) {
					req.SetPathValue("batchID", "11")
					handleRollbackBatch(rec, req, nil)
				},
				wantStatus: http.StatusInternalServerError,
				wantCode:   "server_misconfigured",
			},
			{
				name: "probe host",
				path: "/hosts/host-1/probe",
				run: func(rec *responseRecorder, req *http.Request) {
					req.SetPathValue("hostID", "host-1")
					handleProbeHost(rec, req, nil)
				},
				wantStatus: http.StatusInternalServerError,
				wantCode:   "server_misconfigured",
			},
			{
				name: "create host",
				path: "/hosts",
				run: func(rec *responseRecorder, req *http.Request) {
					handleCreateHost(rec, req, nil)
				},
				wantStatus: http.StatusInternalServerError,
				wantCode:   "server_misconfigured",
			},
		}

		for _, tc := range cases {
			tc := tc
			t.Run(tc.name, func(t *testing.T) {
				req := httptestRequest(t, "POST", tc.path, map[string]any{}, "")
				rec := &responseRecorder{header: map[string][]string{}}
				tc.run(rec, req)
				assertStatusCode(t, rec, tc.wantStatus)
				assertJSONContains(t, rec.Body().Bytes(), "error.code", tc.wantCode)
			})
		}
	})

	t.Run("access wrappers handle missing actions and decode errors", func(t *testing.T) {
		t.Parallel()

		req := httptestRequest(t, "POST", "/providers/deepseek/access/subscriptions", map[string]any{}, "")
		req.SetPathValue("providerID", "deepseek")
		rec := &responseRecorder{header: map[string][]string{}}
		handleAssignAccessSubscriptions(rec, req, nil)
		assertStatusCode(t, rec, http.StatusInternalServerError)
		assertJSONContains(t, rec.Body().Bytes(), "error.code", "server_misconfigured")

		req = httptestRequest(t, "POST", "/providers/deepseek/access/preview", map[string]any{}, "")
		req.SetPathValue("providerID", "deepseek")
		rec = &responseRecorder{header: map[string][]string{}}
		handleAccessPreview(rec, req, nil)
		assertStatusCode(t, rec, http.StatusInternalServerError)
		assertJSONContains(t, rec.Body().Bytes(), "error.code", "server_misconfigured")

		badReq, err := http.NewRequest(http.MethodPost, "/providers/deepseek/access/subscriptions", strings.NewReader("{"))
		if err != nil {
			t.Fatalf("http.NewRequest(assign bad json) error = %v", err)
		}
		badReq.SetPathValue("providerID", "deepseek")
		badReq.Header.Set("Content-Type", "application/json")
		rec = &responseRecorder{header: map[string][]string{}}
		handleAssignAccessSubscriptions(rec, badReq, func(context.Context, AssignAccessSubscriptionsRequest) (AssignAccessSubscriptionsResult, error) {
			t.Fatal("assign action should not be called for bad json")
			return AssignAccessSubscriptionsResult{}, nil
		})
		assertStatusCode(t, rec, http.StatusBadRequest)
		assertJSONContains(t, rec.Body().Bytes(), "error.code", "bad_request")

		badReq, err = http.NewRequest(http.MethodPost, "/providers/deepseek/access/preview", strings.NewReader("{"))
		if err != nil {
			t.Fatalf("http.NewRequest(preview bad json) error = %v", err)
		}
		badReq.SetPathValue("providerID", "deepseek")
		badReq.Header.Set("Content-Type", "application/json")
		rec = &responseRecorder{header: map[string][]string{}}
		handleAccessPreview(rec, badReq, func(context.Context, AccessPreviewRequest) (AccessPreviewResult, error) {
			t.Fatal("access preview action should not be called for bad json")
			return AccessPreviewResult{}, nil
		})
		assertStatusCode(t, rec, http.StatusBadRequest)
		assertJSONContains(t, rec.Body().Bytes(), "error.code", "bad_request")
	})

	t.Run("pack and host wrappers classify action errors", func(t *testing.T) {
		t.Parallel()

		req := httptestRequest(t, "GET", "/packs/openai-cn-pack", map[string]any{}, "")
		req.SetPathValue("packID", "openai-cn-pack")
		rec := &responseRecorder{header: map[string][]string{}}
		handleGetPack(rec, req, func(context.Context, string) (PackInfo, error) {
			return PackInfo{}, sql.ErrNoRows
		})
		assertStatusCode(t, rec, http.StatusInternalServerError)
		assertJSONContains(t, rec.Body().Bytes(), "error.code", "internal_error")

		req = httptestRequest(t, "POST", "/import-batches/11/rollback", map[string]any{}, "")
		req.SetPathValue("batchID", "11")
		rec = &responseRecorder{header: map[string][]string{}}
		handleRollbackBatch(rec, req, func(context.Context, RollbackBatchRequest) (provision.RollbackReport, error) {
			return provision.RollbackReport{}, fmt.Errorf("batch 11 not found")
		})
		assertStatusCode(t, rec, http.StatusNotFound)
		assertJSONContains(t, rec.Body().Bytes(), "error.code", "not_found")

		req = httptestRequest(t, "POST", "/hosts/host-1/probe", map[string]any{
			"auth": map[string]any{"type": "apikey", "token": "host-token"},
		}, "")
		req.SetPathValue("hostID", "host-1")
		rec = &responseRecorder{header: map[string][]string{}}
		handleProbeHost(rec, req, func(context.Context, ProbeHostRequest) (HostInfo, error) {
			return HostInfo{}, fmt.Errorf("host probe decode failed")
		})
		assertStatusCode(t, rec, http.StatusBadRequest)
		assertJSONContains(t, rec.Body().Bytes(), "error.code", "bad_request")

		req = httptestRequest(t, "POST", "/hosts", map[string]any{
			"name": "host-1",
		}, "")
		rec = &responseRecorder{header: map[string][]string{}}
		handleCreateHost(rec, req, func(context.Context, CreateHostRequest) (HostInfo, error) {
			return HostInfo{}, fmt.Errorf("base_url is required")
		})
		assertStatusCode(t, rec, http.StatusBadRequest)
		assertJSONContains(t, rec.Body().Bytes(), "error.code", "bad_request")
	})
}

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

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

	hostPK := createAppHostRecord(t, store, "https://sub2api.example.com")
	packPK := createAppPackRecord(t, store)
	providerPK := createAppProviderRecord(t, store, packPK)
	batchID := createAppBatchRecord(t, store, hostPK, packPK, providerPK)

	if _, err := store.ImportBatchItems().Create(context.Background(), sqlite.ImportBatchItem{
		BatchID:        batchID,
		KeyFingerprint: "sha256:test",
		AccountStatus:  "passed",
	}); err != nil {
		t.Fatalf("ImportBatchItems().Create() error = %v", err)
	}
	for _, resource := range []sqlite.ManagedResource{
		{BatchID: batchID, HostID: hostPK, ResourceType: "group", HostResourceID: "group-1", ResourceName: "group-1"},
		{BatchID: batchID, HostID: hostPK, ResourceType: "account", HostResourceID: "account-1", ResourceName: "account-1"},
	} {
		if _, err := store.ManagedResources().Create(context.Background(), resource); err != nil {
			t.Fatalf("ManagedResources().Create(%s) error = %v", resource.ResourceType, err)
		}
	}
	if _, err := store.AccessClosures().Create(context.Background(), sqlite.AccessClosureRecord{
		BatchID:     batchID,
		ClosureType: provision.AccessModeSelfService,
		Status:      provision.AccessStatusSelfServiceReady,
		DetailsJSON: `{"ok":true}`,
	}); err != nil {
		t.Fatalf("AccessClosures().Create() error = %v", err)
	}
	if _, err := store.ReconcileRuns().Create(context.Background(), sqlite.ReconcileRun{
		BatchID:     batchID,
		HostID:      hostPK,
		ProviderID:  providerPK,
		Status:      "drifted",
		SummaryJSON: `{"missing_count":1}`,
	}); err != nil {
		t.Fatalf("ReconcileRuns().Create() error = %v", err)
	}

	packRow, err := store.Packs().GetByID(context.Background(), packPK)
	if err != nil {
		t.Fatalf("Packs().GetByID() error = %v", err)
	}
	providerRow, err := store.Providers().GetByID(context.Background(), providerPK)
	if err != nil {
		t.Fatalf("Providers().GetByID() error = %v", err)
	}
	hostRow, err := store.Hosts().GetByID(context.Background(), hostPK)
	if err != nil {
		t.Fatalf("Hosts().GetByID() error = %v", err)
	}

	actions := NewActionSet(appTestDSN(t, store))

	detail, err := actions.BatchDetail(context.Background(), BatchDetailRequest{BatchID: batchID})
	if err != nil {
		t.Fatalf("BatchDetail() error = %v", err)
	}
	if detail.Batch.ID != batchID || len(detail.Items) != 1 || len(detail.ManagedResources) != 2 || len(detail.AccessClosures) != 1 || len(detail.ReconcileRuns) != 1 {
		t.Fatalf("BatchDetail() = %+v, want populated batch detail", detail)
	}

	snapshotReq := ProviderQueryRequest{
		ProviderID: providerRow.ProviderID,
		PackID:     packRow.PackID,
		HostID:     hostRow.HostID,
	}
	statusSnapshot, err := actions.GetProviderStatus(context.Background(), snapshotReq)
	if err != nil {
		t.Fatalf("GetProviderStatus() error = %v", err)
	}
	if statusSnapshot.Batch.ID != batchID || statusSnapshot.LatestAccessStatus != provision.AccessStatusSelfServiceReady || statusSnapshot.LatestReconcileStatus != "drifted" || statusSnapshot.ProviderStatus != provision.ProviderStatusActive {
		t.Fatalf("GetProviderStatus() = %+v, want active snapshot with drifted reconcile metadata", statusSnapshot)
	}
	if got := statusSnapshot.LatestReconcileSummary["missing_count"]; got != float64(1) {
		t.Fatalf("LatestReconcileSummary[missing_count] = %#v, want 1", got)
	}

	resourceSnapshot, err := actions.GetProviderResources(context.Background(), snapshotReq)
	if err != nil {
		t.Fatalf("GetProviderResources() error = %v", err)
	}
	if len(resourceSnapshot.ManagedResources) != 2 || len(resourceSnapshot.AccessClosures) != 1 || len(resourceSnapshot.ReconcileRuns) != 1 {
		t.Fatalf("GetProviderResources() = %+v, want persisted resources/closures/runs", resourceSnapshot)
	}

	accessSnapshot, err := actions.GetProviderAccessStatus(context.Background(), snapshotReq)
	if err != nil {
		t.Fatalf("GetProviderAccessStatus() error = %v", err)
	}
	if accessSnapshot.Batch.ID != batchID || accessSnapshot.LatestAccessStatus != provision.AccessStatusSelfServiceReady || len(accessSnapshot.AccessClosures) != 1 {
		t.Fatalf("GetProviderAccessStatus() = %+v, want latest access closure", accessSnapshot)
	}
}

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

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodDelete {
			t.Fatalf("method = %s, want DELETE", r.Method)
		}
		switch r.URL.Path {
		case "/api/v1/admin/accounts/account-1",
			"/api/v1/admin/payment/plans/plan-1",
			"/api/v1/admin/channels/channel-1",
			"/api/v1/admin/groups/group-1":
			w.WriteHeader(http.StatusNoContent)
		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

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

	hostPK := createAppHostRecord(t, store, server.URL)
	packPK := createAppPackRecord(t, store)
	providerPK := createAppProviderRecord(t, store, packPK)
	batchID := createAppBatchRecord(t, store, hostPK, packPK, providerPK)
	for _, resource := range []sqlite.ManagedResource{
		{BatchID: batchID, HostID: hostPK, ResourceType: "group", HostResourceID: "group-1", ResourceName: "group-1"},
		{BatchID: batchID, HostID: hostPK, ResourceType: "channel", HostResourceID: "channel-1", ResourceName: "channel-1"},
		{BatchID: batchID, HostID: hostPK, ResourceType: "plan", HostResourceID: "plan-1", ResourceName: "plan-1"},
		{BatchID: batchID, HostID: hostPK, ResourceType: "account", HostResourceID: "account-1", ResourceName: "account-1"},
	} {
		if _, err := store.ManagedResources().Create(context.Background(), resource); err != nil {
			t.Fatalf("ManagedResources().Create(%s) error = %v", resource.ResourceType, err)
		}
	}

	actions := NewActionSet(appTestDSN(t, store))
	report, err := actions.RollbackBatch(context.Background(), RollbackBatchRequest{BatchID: batchID})
	if err != nil {
		t.Fatalf("RollbackBatch() error = %v", err)
	}
	if report.AccountsDeleted != 1 || report.PlansDeleted != 1 || report.ChannelsDeleted != 1 || report.GroupsDeleted != 1 {
		t.Fatalf("RollbackBatch() = %+v, want one deleted resource per type", report)
	}

	batchRow, err := store.ImportBatches().GetByID(context.Background(), batchID)
	if err != nil {
		t.Fatalf("ImportBatches().GetByID() error = %v", err)
	}
	if batchRow.BatchStatus != provision.BatchStatusRolledBack {
		t.Fatalf("batch status = %q, want %q", batchRow.BatchStatus, provision.BatchStatusRolledBack)
	}
}

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

	t.Run("update existing host connection", func(t *testing.T) {
		t.Parallel()

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

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

		if _, err := store.Hosts().Create(context.Background(), sqlite.Host{
			HostID:              "prod-sub2api",
			BaseURL:             server.URL,
			HostVersion:         "0.0.1",
			CapabilityProbeJSON: "{}",
			AuthType:            "apikey",
			AuthToken:           "old-token",
		}); err != nil {
			t.Fatalf("Hosts().Create() error = %v", err)
		}

		actions := NewActionSet(appTestDSN(t, store))
		host, err := actions.CreateHost(context.Background(), CreateHostRequest{
			Name:    "prod-sub2api",
			BaseURL: server.URL,
			Auth:    CreateHostAuth{Type: "apikey", Token: "host-token"},
		})
		if err != nil {
			t.Fatalf("CreateHost(update) error = %v", err)
		}
		if host.HostVersion != "0.1.126" || host.Status != "supported" {
			t.Fatalf("CreateHost(update) = %+v, want reprobed supported host", host)
		}

		stored, err := store.Hosts().GetByHostID(context.Background(), "prod-sub2api")
		if err != nil {
			t.Fatalf("Hosts().GetByHostID() error = %v", err)
		}
		if stored.AuthToken != "host-token" || stored.HostVersion != "0.1.126" {
			t.Fatalf("stored host = %+v, want updated token/version", stored)
		}
	})

	t.Run("base url conflict returns 409", func(t *testing.T) {
		t.Parallel()

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

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

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

		actions := NewActionSet(appTestDSN(t, store))
		_, err := actions.CreateHost(context.Background(), CreateHostRequest{
			Name:    "new-host",
			BaseURL: server.URL,
			Auth:    CreateHostAuth{Type: "apikey", Token: "host-token"},
		})
		if err == nil {
			t.Fatal("CreateHost(conflict) error = nil, want conflict")
		}
		httpErr, ok := err.(*httpError)
		if !ok || httpErr.StatusCode != http.StatusConflict {
			t.Fatalf("CreateHost(conflict) error = %T %v, want *httpError 409", err, err)
		}
	})
}

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

	baseHandler := newBatchImportActionStubServer(t)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Method == http.MethodDelete {
			switch r.URL.Path {
			case "/api/v1/admin/accounts/account-1",
				"/api/v1/admin/payment/plans/plan-1",
				"/api/v1/admin/channels/channel-1",
				"/api/v1/admin/groups/group-1":
				w.WriteHeader(http.StatusNoContent)
				return
			}
		}
		baseHandler.ServeHTTP(w, r)
	}))
	defer server.Close()

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

	hostPK := createAppHostRecord(t, store, server.URL)
	actions := NewActionSet(appTestDSN(t, store))
	packPath := filepath.Join("..", "..", "packs", "openai-cn-pack")

	installResult, err := actions.InstallPack(context.Background(), InstallPackRequest{
		HostBaseURL: server.URL,
		HostAPIKey:  "host-token",
		PackPath:    packPath,
	})
	if err != nil {
		t.Fatalf("InstallPack() error = %v", err)
	}
	if installResult.Pack.PackID != "openai-cn-pack" || len(installResult.Providers) == 0 {
		t.Fatalf("InstallPack() = %+v, want persisted pack with providers", installResult)
	}

	preview, err := actions.PreviewProvider(context.Background(), PreviewProviderRequest{
		HostBaseURL: server.URL,
		PackPath:    packPath,
		ProviderID:  "deepseek",
		Mode:        provision.ImportModePartial,
		Keys:        []string{" key-1 ", "key-2"},
	})
	if err != nil {
		t.Fatalf("PreviewProvider() error = %v", err)
	}
	if len(preview.AcceptedKeys) != 2 || preview.Decisions["group"].Action != provision.PreviewActionCreate {
		t.Fatalf("PreviewProvider() = %+v, want accepted keys and create decisions", preview)
	}

	packRow, err := store.Packs().GetByPackID(context.Background(), "openai-cn-pack")
	if err != nil {
		t.Fatalf("Packs().GetByPackID() error = %v", err)
	}
	providerRow, err := store.Providers().GetByPackIDAndProviderID(context.Background(), packRow.ID, "deepseek")
	if err != nil {
		t.Fatalf("Providers().GetByPackIDAndProviderID() error = %v", err)
	}
	batchID, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       hostPK,
		PackID:       packRow.ID,
		ProviderID:   providerRow.ID,
		Mode:         provision.ImportModePartial,
		BatchStatus:  provision.BatchStatusSucceeded,
		AccessStatus: provision.AccessStatusSelfServiceReady,
	})
	if err != nil {
		t.Fatalf("ImportBatches().Create() error = %v", err)
	}
	for _, resource := range []sqlite.ManagedResource{
		{BatchID: batchID, HostID: hostPK, ResourceType: "group", HostResourceID: "group-1", ResourceName: "group-1"},
		{BatchID: batchID, HostID: hostPK, ResourceType: "channel", HostResourceID: "channel-1", ResourceName: "channel-1"},
		{BatchID: batchID, HostID: hostPK, ResourceType: "plan", HostResourceID: "plan-1", ResourceName: "plan-1"},
		{BatchID: batchID, HostID: hostPK, ResourceType: "account", HostResourceID: "account-1", ResourceName: "account-1"},
	} {
		if _, err := store.ManagedResources().Create(context.Background(), resource); err != nil {
			t.Fatalf("ManagedResources().Create(%s) error = %v", resource.ResourceType, err)
		}
	}

	report, err := actions.RollbackProvider(context.Background(), RollbackProviderRequest{
		HostBaseURL: server.URL,
		PackPath:    packPath,
		ProviderID:  "deepseek",
	})
	if err != nil {
		t.Fatalf("RollbackProvider() error = %v", err)
	}
	if report.AccountsDeleted != 1 || report.PlansDeleted != 1 || report.ChannelsDeleted != 1 || report.GroupsDeleted != 1 {
		t.Fatalf("RollbackProvider() = %+v, want one deleted resource per type", report)
	}

	batchRow, err := store.ImportBatches().GetByID(context.Background(), batchID)
	if err != nil {
		t.Fatalf("ImportBatches().GetByID() error = %v", err)
	}
	if batchRow.BatchStatus != provision.BatchStatusRolledBack {
		t.Fatalf("batch status = %q, want %q", batchRow.BatchStatus, provision.BatchStatusRolledBack)
	}
}

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

	baseHandler := newBatchImportActionStubServer(t)
	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{}}})
			return
		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"}})
			return
		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}})
			return
		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}})
			return
		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"}})
			return
		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-test", "name": "managed-key"}})
			return
		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}}})
			return
		case r.URL.Path == "/v1/models" && strings.HasPrefix(strings.TrimSpace(r.Header.Get("Authorization")), "Bearer sk-relay-"):
			writeJSON(w, http.StatusOK, map[string]any{"data": []map[string]any{{"id": "deepseek-v4-pro"}}})
			return
		case r.URL.Path == "/v1/chat/completions" && strings.HasPrefix(strings.TrimSpace(r.Header.Get("Authorization")), "Bearer sk-relay-"):
			writeJSON(w, http.StatusOK, map[string]any{
				"id": "chatcmpl_subscription",
				"choices": []map[string]any{{
					"index": 0,
					"message": map[string]any{
						"role":    "assistant",
						"content": "pong",
					},
				}},
			})
			return
		}
		baseHandler.ServeHTTP(w, r)
	}))
	defer server.Close()

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

	hostPK := createAppHostRecord(t, store, server.URL)
	actions := NewActionSet(appTestDSN(t, store))
	packPath := filepath.Join("..", "..", "packs", "openai-cn-pack")

	if _, err := actions.InstallPack(context.Background(), InstallPackRequest{
		HostBaseURL: server.URL,
		HostAPIKey:  "host-token",
		PackPath:    packPath,
	}); err != nil {
		t.Fatalf("InstallPack() error = %v", err)
	}
	packRow, err := store.Packs().GetByPackID(context.Background(), "openai-cn-pack")
	if err != nil {
		t.Fatalf("Packs().GetByPackID() error = %v", err)
	}
	providerRow, err := store.Providers().GetByPackIDAndProviderID(context.Background(), packRow.ID, "deepseek")
	if err != nil {
		t.Fatalf("Providers().GetByPackIDAndProviderID() error = %v", err)
	}
	batchID, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       hostPK,
		PackID:       packRow.ID,
		ProviderID:   providerRow.ID,
		Mode:         provision.ImportModePartial,
		BatchStatus:  provision.BatchStatusSucceeded,
		AccessStatus: provision.AccessStatusSelfServiceReady,
	})
	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)
	}

	result, err := actions.AssignAccessSubscriptions(context.Background(), AssignAccessSubscriptionsRequest{
		HostBaseURL:       server.URL,
		PackPath:          packPath,
		ProviderID:        "deepseek",
		AccessAPIKey:      "caller-probe-key",
		SubscriptionUsers: []string{"crm-user-1"},
		SubscriptionDays:  30,
	})
	if err != nil {
		t.Fatalf("AssignAccessSubscriptions() error = %v", err)
	}
	if result.Assigned != 1 || result.AccessStatus != provision.AccessStatusSubscriptionReady {
		t.Fatalf("AssignAccessSubscriptions() = %+v, want one assigned subscription_ready result", result)
	}

	closures, err := store.AccessClosures().GetByBatchID(context.Background(), batchID)
	if err != nil {
		t.Fatalf("AccessClosures().GetByBatchID() error = %v", err)
	}
	if len(closures) != 1 || closures[0].Status != provision.AccessStatusSubscriptionReady {
		t.Fatalf("AccessClosures() = %+v, want persisted subscription_ready closure", closures)
	}
	var payload map[string]any
	if err := json.Unmarshal([]byte(closures[0].DetailsJSON), &payload); err != nil {
		t.Fatalf("decode access closure payload: %v", err)
	}
	if _, ok := payload["probe_api_key"]; ok {
		t.Fatalf("subscription access closure should omit probe_api_key, got %#v", payload["probe_api_key"])
	}
	if got, _ := payload["requested_probe_api_key"].(string); got != "caller-probe-key" {
		t.Fatalf("requested_probe_api_key = %q, want caller-probe-key", got)
	}
	if got, _ := payload["effective_probe_key_source"].(string); got != "managed_subscription" {
		t.Fatalf("effective_probe_key_source = %q, want managed_subscription", got)
	}
	batchRow, err := store.ImportBatches().GetByID(context.Background(), batchID)
	if err != nil {
		t.Fatalf("ImportBatches().GetByID() error = %v", err)
	}
	if batchRow.AccessStatus != provision.AccessStatusSubscriptionReady {
		t.Fatalf("batch access_status = %q, want %q", batchRow.AccessStatus, provision.AccessStatusSubscriptionReady)
	}
}

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

	baseHandler := newBatchImportActionStubServer(t)
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/api/v1/admin/groups":
			if r.Method == http.MethodGet {
				writeJSON(w, http.StatusOK, map[string]any{"data": []map[string]any{{"id": "group_1", "name": "DeepSeek 默认分组-self-service"}}})
				return
			}
			if r.Method == http.MethodPut {
				writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": "group_1", "name": "DeepSeek 默认分组-self-service"}})
				return
			}
		case "/api/v1/admin/channels":
			if r.Method == http.MethodGet {
				writeJSON(w, http.StatusOK, map[string]any{"data": []map[string]any{{"id": "channel_1", "name": "DeepSeek 默认渠道-self-service"}}})
				return
			}
		case "/api/v1/admin/groups/group_1":
			if r.Method == http.MethodPut {
				writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": "group_1", "name": "DeepSeek 默认分组-self-service"}})
				return
			}
		case "/api/v1/admin/channels/channel_1":
			if r.Method == http.MethodPut {
				writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"id": "channel_1", "name": "DeepSeek 默认渠道-self-service"}})
				return
			}
		case "/api/v1/admin/accounts":
			if r.Method == http.MethodGet {
				writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"items": []map[string]any{{"id": "account_1", "name": "deepseek-01"}}, "pages": 1}})
				return
			}
		case "/api/v1/admin/accounts/account_1/models":
			writeJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"items": []map[string]any{{"id": "deepseek-v4-pro", "display_name": "DeepSeek V4 Pro", "type": "chat"}}}})
			return
		case "/api/v1/admin/accounts/account_1":
			if r.Method == http.MethodDelete {
				w.WriteHeader(http.StatusNoContent)
				return
			}
		case "/v1/models":
			switch strings.TrimSpace(r.Header.Get("Authorization")) {
			case "Bearer entry-key", "Bearer gateway-key":
				writeJSON(w, http.StatusOK, map[string]any{"data": []map[string]any{{"id": "deepseek-v4-pro"}}})
				return
			}
		case "/v1/chat/completions":
			switch strings.TrimSpace(r.Header.Get("Authorization")) {
			case "Bearer entry-key", "Bearer gateway-key":
				writeJSON(w, http.StatusOK, map[string]any{
					"id": "chatcmpl_deepseek",
					"choices": []map[string]any{{
						"index": 0,
						"message": map[string]any{
							"role":    "assistant",
							"content": "pong",
						},
					}},
				})
				return
			}
		}
		baseHandler.ServeHTTP(w, r)
	}))
	defer server.Close()

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

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

	actions := NewActionSet(appTestDSN(t, store))
	packPath := filepath.Join("..", "..", "packs", "openai-cn-pack")

	importResult, err := actions.ImportProvider(context.Background(), ImportProviderRequest{
		HostID:       "host-deepseek",
		PackPath:     packPath,
		ProviderID:   "deepseek",
		Keys:         []string{"entry-key"},
		Mode:         provision.ImportModePartial,
		AccessMode:   provision.AccessModeSelfService,
		AccessAPIKey: "gateway-key",
	})
	if err != nil {
		t.Fatalf("ImportProvider() error = %v", err)
	}
	if importResult.BatchID <= 0 || importResult.Report.BatchStatus != provision.BatchStatusSucceeded || importResult.Report.AccessStatus != provision.AccessStatusSelfServiceReady {
		t.Fatalf("ImportProvider() = %+v, want succeeded self_service_ready batch", importResult)
	}

	reconcileResult, err := actions.ReconcileProvider(context.Background(), ReconcileProviderRequest{
		HostID:       "host-deepseek",
		PackPath:     packPath,
		ProviderID:   "deepseek",
		AccessAPIKey: "gateway-key",
	})
	if err != nil {
		t.Fatalf("ReconcileProvider() error = %v", err)
	}
	if reconcileResult.BatchID != importResult.BatchID || reconcileResult.Status != "active" || reconcileResult.AccessStatus != provision.AccessStatusSelfServiceReady {
		t.Fatalf("ReconcileProvider() = %+v, want active reconcile for imported batch", reconcileResult)
	}
}

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

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

	mustCreateAppImportRun(t, store, sqlite.ImportRun{
		RunID:      "run-1",
		HostID:     "host-1",
		Mode:       "partial",
		AccessMode: "self_service",
		State:      "running",
	})
	mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:00' WHERE run_id = 'run-1'`)
	mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
		ItemID:              "item-1",
		RunID:               "run-1",
		BaseURL:             "https://a.example.com/v1",
		ProviderID:          "a",
		APIKeyFingerprint:   "sha256:a",
		CurrentStage:        "done",
		ConfirmationStatus:  "confirmed",
		AccessStatus:        "active",
		MatchedAccountState: "active",
		AccountResolution:   "created",
	})
	mustCreateAppImportRunItem(t, store, sqlite.ImportRunItem{
		ItemID:              "item-2",
		RunID:               "run-1",
		BaseURL:             "https://b.example.com/v1",
		ProviderID:          "b",
		APIKeyFingerprint:   "sha256:b",
		CurrentStage:        "done",
		ConfirmationStatus:  "confirmed",
		AccessStatus:        "active",
		MatchedAccountState: "active",
		AccountResolution:   "created",
	})

	action := buildListBatchImportRunItemsAction(appTestDSN(t, store))
	result, err := action(context.Background(), ListBatchImportRunItemsRequest{
		RunID:  "run-1",
		Cursor: "item-1",
		Limit:  1,
	})
	if err != nil {
		t.Fatalf("buildListBatchImportRunItemsAction(cursor) error = %v", err)
	}
	if len(result.Items) != 1 || result.Items[0].ItemID != "item-2" {
		t.Fatalf("result.Items = %+v, want [item-2]", result.Items)
	}
}

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

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

	mustCreateAppImportRun(t, store, sqlite.ImportRun{RunID: "run-1", HostID: "host-1", Mode: "partial", AccessMode: "self_service", State: "running"})
	mustCreateAppImportRun(t, store, sqlite.ImportRun{RunID: "run-2", HostID: "host-1", Mode: "partial", AccessMode: "self_service", State: "running"})
	mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:02' WHERE run_id = 'run-1'`)
	mustExecSQL(t, store, `UPDATE import_runs SET started_at = '2026-05-23 10:00:01' WHERE run_id = 'run-2'`)

	action := buildListBatchImportRunsAction(appTestDSN(t, store))
	result, err := action(context.Background(), ListBatchImportRunsRequest{Cursor: "run-1", Limit: -1})
	if err != nil {
		t.Fatalf("buildListBatchImportRunsAction(cursor) error = %v", err)
	}
	if len(result.Runs) != 1 || result.Runs[0].RunID != "run-2" {
		t.Fatalf("result.Runs = %+v, want [run-2]", result.Runs)
	}
}

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

	action := buildGetBatchImportRunAction("file:/definitely-missing-path/does-not-exist.db?mode=ro")
	if _, err := action(context.Background(), "run-1"); err == nil {
		t.Fatal("buildGetBatchImportRunAction() error = nil, want open db error")
	}
}

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

	action := buildListBatchImportRunItemsAction("file:/definitely-missing-path/does-not-exist.db?mode=ro")
	if _, err := action(context.Background(), ListBatchImportRunItemsRequest{RunID: "run-1"}); err == nil {
		t.Fatal("buildListBatchImportRunItemsAction() error = nil, want open db error")
	}
}

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

	action := buildListBatchImportRunsAction("file:/definitely-missing-path/does-not-exist.db?mode=ro")
	if _, err := action(context.Background(), ListBatchImportRunsRequest{}); err == nil {
		t.Fatal("buildListBatchImportRunsAction() error = nil, want open db error")
	}
}

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

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

	action := buildGetBatchImportRunItemAction(appTestDSN(t, store))
	if _, err := action(context.Background(), GetBatchImportRunItemRequest{RunID: "run-1", ItemID: "missing"}); err == nil || err.Error() != "item not found: missing" {
		t.Fatalf("missing item error = %v, want item not found", err)
	}
}

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

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

	mustCreateAppImportRun(t, store, sqlite.ImportRun{
		RunID:      "run-1",
		HostID:     "host-1",
		Mode:       "partial",
		AccessMode: "self_service",
		State:      "completed",
	})
	if err := resumePendingBatchImportRuns(context.Background(), appTestDSN(t, store)); err != nil {
		t.Fatalf("resumePendingBatchImportRuns() error = %v", err)
	}
}

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

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

	if _, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:      "host-1",
		BaseURL:     "https://sub2api.example.com",
		HostVersion: "0.1.126",
		AuthToken:   "host-token",
	}); err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	runner, err := newBatchImportRuntimeRunnerFromStoredRun(context.Background(), store, sqlite.ImportRun{
		RunID:                 "run-1",
		HostID:                "host-1",
		Mode:                  "partial",
		AccessMode:            "subscription",
		SubscriptionUsersJSON: `["user-1","user-2"]`,
		SubscriptionDays:      30,
		ProbeAPIKey:           "probe-key",
	})
	if err != nil {
		t.Fatalf("newBatchImportRuntimeRunnerFromStoredRun() error = %v", err)
	}
	if runner.request.HostID != "host-1" || len(runner.request.SubscriptionUsers) != 2 || runner.request.ProbeAPIKey != "probe-key" {
		t.Fatalf("runner.request = %+v, want parsed stored run request", runner.request)
	}
}

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

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

	action := buildGetBatchImportRunAction(appTestDSN(t, store))
	_, err := action(context.Background(), "missing")
	if err == nil || err.Error() != "run not found: missing" {
		t.Fatalf("missing run error = %v, want run not found", err)
	}
}

func TestSQLNoRowsReference(t *testing.T) {
	t.Parallel()
	if sql.ErrNoRows == nil {
		t.Fatal("sql.ErrNoRows = nil")
	}
}
