package reconcile

import (
	"context"
	"errors"
	"fmt"
	"testing"

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

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	itemID := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", "{")

	svc := NewService(store, &reconcileHostStub{})
	_, err := svc.rerunAccountProbes(context.Background(), []sqlite.ImportBatchItem{{
		ID:               itemID,
		BatchID:          fixture.batchID,
		KeyFingerprint:   "fp-1",
		AccountStatus:    "pending",
		ProbeSummaryJSON: "{",
	}}, "deepseek-chat")
	if err == nil || err.Error() != fmt.Sprintf("decode import batch item %d probe summary: unexpected end of JSON input", itemID) {
		t.Fatalf("rerunAccountProbes() error = %v, want decode error", err)
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	itemID := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", `{}`)

	svc := NewService(store, &reconcileHostStub{})
	_, err := svc.rerunAccountProbes(context.Background(), []sqlite.ImportBatchItem{{
		ID:               itemID,
		BatchID:          fixture.batchID,
		KeyFingerprint:   "fp-1",
		AccountStatus:    "pending",
		ProbeSummaryJSON: `{}`,
	}}, "deepseek-chat")
	if err == nil || err.Error() != fmt.Sprintf("import batch item %d missing account_id in probe summary", itemID) {
		t.Fatalf("rerunAccountProbes() error = %v, want missing account_id", err)
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	itemID := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", `{"account_id":"account-1"}`)

	host := &reconcileHostStub{
		testErrors: map[string]error{"account-1": errors.New("probe failed")},
	}
	svc := NewService(store, host)
	_, err := svc.rerunAccountProbes(context.Background(), []sqlite.ImportBatchItem{{
		ID:               itemID,
		BatchID:          fixture.batchID,
		KeyFingerprint:   "fp-1",
		AccountStatus:    "pending",
		ProbeSummaryJSON: `{"account_id":"account-1"}`,
	}}, "deepseek-chat")
	if err == nil || err.Error() != "re-test account account-1: probe failed" {
		t.Fatalf("rerunAccountProbes() error = %v, want retest failure", err)
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	itemID := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", `{"account_id":"account-1"}`)

	host := &reconcileHostStub{
		testResults: map[string]sub2api.ProbeResult{
			"account-1": {OK: true, Status: "passed"},
		},
		modelErrors: map[string]error{"account-1": errors.New("models unavailable")},
	}
	svc := NewService(store, host)
	_, err := svc.rerunAccountProbes(context.Background(), []sqlite.ImportBatchItem{{
		ID:               itemID,
		BatchID:          fixture.batchID,
		KeyFingerprint:   "fp-1",
		AccountStatus:    "pending",
		ProbeSummaryJSON: `{"account_id":"account-1"}`,
	}}, "deepseek-chat")
	if err == nil || err.Error() != "reload account models account-1: models unavailable" {
		t.Fatalf("rerunAccountProbes() error = %v, want model reload failure", err)
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	itemID1 := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", `{"account_id":"account-1"}`)
	itemID2 := mustCreateImportBatchItem(t, store, fixture.batchID, "fp-2", `{"account_id":" account-1 "}`)

	host := &reconcileHostStub{
		testResults: map[string]sub2api.ProbeResult{
			"account-1": {
				OK:      false,
				Status:  "failed",
				Message: "API returned 403: Forbidden",
			},
		},
		models: map[string][]sub2api.AccountModel{
			"account-1": {{ID: "deepseek-chat"}},
		},
	}
	svc := NewService(store, host)
	summary, err := svc.rerunAccountProbes(context.Background(), []sqlite.ImportBatchItem{
		{
			ID:               itemID1,
			BatchID:          fixture.batchID,
			KeyFingerprint:   "fp-1",
			AccountStatus:    "pending",
			ProbeSummaryJSON: `{"account_id":"account-1"}`,
		},
		{
			ID:               itemID2,
			BatchID:          fixture.batchID,
			KeyFingerprint:   "fp-2",
			AccountStatus:    "pending",
			ProbeSummaryJSON: `{"account_id":" account-1 "}`,
		},
	}, "deepseek-chat")
	if err != nil {
		t.Fatalf("rerunAccountProbes() error = %v", err)
	}
	if summary.Failures != 0 {
		t.Fatalf("summary.Failures = %d, want 0 for advisory warning", summary.Failures)
	}
	if !summary.ResponsesCapabilitySuspect {
		t.Fatal("summary.ResponsesCapabilitySuspect = false, want true")
	}
	if len(summary.AccountIDs) != 1 || summary.AccountIDs[0] != "account-1" {
		t.Fatalf("summary.AccountIDs = %v, want [account-1]", summary.AccountIDs)
	}
	for _, itemID := range []int64{itemID1, itemID2} {
		items, err := store.ImportBatchItems().GetByBatchID(context.Background(), fixture.batchID)
		if err != nil {
			t.Fatalf("ImportBatchItems().GetByBatchID() error = %v", err)
		}
		var got sqlite.ImportBatchItem
		for _, item := range items {
			if item.ID == itemID {
				got = item
				break
			}
		}
		if got.AccountStatus != accountStatusWarning {
			t.Fatalf("item %d AccountStatus = %q, want %q", itemID, got.AccountStatus, accountStatusWarning)
		}
		probes, err := store.ProbeResults().GetByBatchItemID(context.Background(), itemID)
		if err != nil {
			t.Fatalf("ProbeResults().GetByBatchItemID(%d) error = %v", itemID, err)
		}
		if len(probes) != 1 || probes[0].Status != accountStatusWarning {
			t.Fatalf("probe results for item %d = %+v, want single warning result", itemID, probes)
		}
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	closures := []sqlite.AccessClosureRecord{{
		BatchID:     fixture.batchID,
		ClosureType: accessModeSelfService,
		Status:      accessStatusSelfServiceReady,
	}}

	status, checked, err := NewService(store, &reconcileHostStub{}).rerunAccessClosure(
		context.Background(),
		fixture.batchID,
		closures,
		"",
		"deepseek-chat",
		nil,
		false,
	)
	if err != nil {
		t.Fatalf("rerunAccessClosure() error = %v", err)
	}
	if checked {
		t.Fatal("checked = true, want false without probe api key")
	}
	if status != accessStatusSelfServiceReady {
		t.Fatalf("status = %q, want %q", status, accessStatusSelfServiceReady)
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	closures := mustCreateAndLoadAccessClosures(t, store, fixture.batchID, sqlite.AccessClosureRecord{
		BatchID:     fixture.batchID,
		ClosureType: accessModeSelfService,
		Status:      accessStatusSelfServiceReady,
	})

	host := &reconcileHostStub{gatewayErr: errors.New("gateway down")}
	_, _, err := NewService(store, host).rerunAccessClosure(context.Background(), fixture.batchID, closures, "user-key", "deepseek-chat", nil, false)
	if err == nil || err.Error() != "re-check gateway access: gateway down" {
		t.Fatalf("rerunAccessClosure() error = %v, want gateway failure", err)
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	closures := mustCreateAndLoadAccessClosures(t, store, fixture.batchID, sqlite.AccessClosureRecord{
		BatchID:     fixture.batchID,
		ClosureType: accessModeSelfService,
		Status:      accessStatusSelfServiceReady,
	})

	host := &reconcileHostStub{
		gatewayResult: sub2api.GatewayAccessResult{OK: true, HasExpectedModel: true},
		completionErrs: []error{
			errors.New("completion failed"),
		},
	}
	_, _, err := NewService(store, host).rerunAccessClosure(context.Background(), fixture.batchID, closures, "user-key", "deepseek-chat", nil, false)
	if err == nil || err.Error() != "re-check gateway completion: completion failed" {
		t.Fatalf("rerunAccessClosure() error = %v, want completion failure", err)
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	closures := mustCreateAndLoadAccessClosures(t, store, fixture.batchID, sqlite.AccessClosureRecord{
		BatchID:     fixture.batchID,
		ClosureType: accessModeSelfService,
		Status:      accessStatusSelfServiceReady,
	})

	host := &reconcileHostStub{
		gatewayResult: sub2api.GatewayAccessResult{OK: true, HasExpectedModel: true},
		completionResults: []sub2api.GatewayCompletionResult{
			{OK: false, StatusCode: 502, BodyPreview: `{"error":{"message":"No available accounts"}}`},
		},
		completionErrs: []error{
			nil,
			errors.New("still failing"),
		},
	}
	_, _, err := NewService(store, host).rerunAccessClosure(context.Background(), fixture.batchID, closures, "user-key", "deepseek-chat", []string{"account-1"}, true)
	if err == nil || err.Error() != "re-check gateway completion after capability repair: still failing" {
		t.Fatalf("rerunAccessClosure() error = %v, want post-repair completion failure", err)
	}
	if host.clearTempUnschedulableCalls != 1 {
		t.Fatalf("clearTempUnschedulableCalls = %d, want 1", host.clearTempUnschedulableCalls)
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	closures := mustCreateAndLoadAccessClosures(t, store, fixture.batchID, sqlite.AccessClosureRecord{
		BatchID:     fixture.batchID,
		ClosureType: accessModeSelfService,
		Status:      accessStatusSelfServiceReady,
	})

	host := &reconcileHostStub{
		gatewayResult: sub2api.GatewayAccessResult{OK: true, StatusCode: 200, HasExpectedModel: true, Models: []string{"deepseek-chat"}},
		completionResults: []sub2api.GatewayCompletionResult{
			{OK: false, StatusCode: 502, ContentType: "application/json", BodyPreview: `{"error":{"message":"Upstream service temporarily unavailable"}}`},
		},
		disableResponsesErr: errors.New("host rejected update"),
	}
	status, checked, err := NewService(store, host).rerunAccessClosure(context.Background(), fixture.batchID, closures, "user-key", "deepseek-chat", []string{"account-1"}, true)
	if err != nil {
		t.Fatalf("rerunAccessClosure() error = %v", err)
	}
	if !checked {
		t.Fatal("checked = false, want true")
	}
	if status != accessStatusBroken {
		t.Fatalf("status = %q, want %q", status, accessStatusBroken)
	}
	if host.disableResponsesCalls != 1 {
		t.Fatalf("disableResponsesCalls = %d, want 1", host.disableResponsesCalls)
	}
	if host.clearTempUnschedulableCalls != 0 {
		t.Fatalf("clearTempUnschedulableCalls = %d, want 0 after disable failure", host.clearTempUnschedulableCalls)
	}
	if host.completionCalls != 1 {
		t.Fatalf("completionCalls = %d, want 1 without retry after disable failure", host.completionCalls)
	}
	records, err := store.AccessClosures().GetByBatchID(context.Background(), fixture.batchID)
	if err != nil {
		t.Fatalf("AccessClosures().GetByBatchID() error = %v", err)
	}
	if len(records) != 2 {
		t.Fatalf("access closure records = %d, want 2", len(records))
	}
	if records[1].Status != accessStatusBroken {
		t.Fatalf("persisted access closure status = %q, want %q", records[1].Status, accessStatusBroken)
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	otherBatchID := mustCreateImportBatch(t, store, fixture.hostPK, fixture.packPK, fixture.providerPK, "partial", "succeeded", "self_service_ready")
	mustCreateManagedResource(t, store, fixture.batchID, fixture.hostPK, "group", "group-1", "group one")
	mustCreateManagedResource(t, store, fixture.batchID, fixture.hostPK, "account", "account-1", "account one")
	mustCreateManagedResource(t, store, otherBatchID, fixture.hostPK, "group", "group-2", "shared group")
	mustCreateManagedResource(t, store, otherBatchID, fixture.hostPK, "channel", "channel-2", "shared channel")
	mustCreateManagedResource(t, store, otherBatchID, fixture.hostPK, "plan", "plan-2", "shared plan")
	mustCreateManagedResource(t, store, otherBatchID, fixture.hostPK, "account", "account-2", "shared account should not merge")

	got, err := NewService(store, &reconcileHostStub{}).storedResourcesForReconcile(context.Background(), fixture.providerPK, fixture.hostPK, fixture.batchID)
	if err != nil {
		t.Fatalf("storedResourcesForReconcile() error = %v", err)
	}
	if len(got) != 5 {
		t.Fatalf("storedResourcesForReconcile() len = %d, want 5; values = %+v", len(got), got)
	}
	want := map[string]bool{
		"group:group-1":     false,
		"group:group-2":     false,
		"account:account-1": false,
		"channel:channel-2": false,
		"plan:plan-2":       false,
	}
	for _, resource := range got {
		key := resource.ResourceType + ":" + resource.HostResourceID
		if _, ok := want[key]; !ok {
			t.Fatalf("unexpected merged resource %q in %+v", key, got)
		}
		want[key] = true
	}
	for key, seen := range want {
		if !seen {
			t.Fatalf("missing merged resource %q in %+v", key, got)
		}
	}
}

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

	req := Request{}
	if _, err := NewService(nil, &reconcileHostStub{}).Reconcile(context.Background(), req); err == nil || err.Error() != "store is required" {
		t.Fatalf("Reconcile(nil store) error = %v, want store is required", err)
	}

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	if _, err := NewService(store, nil).Reconcile(context.Background(), req); err == nil || err.Error() != "host adapter is required" {
		t.Fatalf("Reconcile(nil host) error = %v, want host adapter is required", err)
	}
	if _, err := NewService(store, &reconcileHostStub{}).Reconcile(context.Background(), req); err == nil || err.Error() != "host_id is required" {
		t.Fatalf("Reconcile(missing host_id) error = %v, want host_id is required", err)
	}
	if _, err := NewService(store, &reconcileHostStub{}).Reconcile(context.Background(), Request{HostID: "host-1"}); err == nil || err.Error() != "host_base_url is required" {
		t.Fatalf("Reconcile(missing host_base_url) error = %v, want host_base_url is required", err)
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	mustExecReconcileSQL(t, store, `UPDATE import_batches SET batch_status = 'failed' WHERE id = ?`, fixture.batchID)

	_, err := NewService(store, &reconcileHostStub{}).Reconcile(context.Background(), Request{
		HostID:            "host-1",
		HostBaseURL:       "https://sub2api.example.com",
		AccessProbeAPIKey: "user-key",
		Pack: pack.LoadedPack{
			Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api"},
		},
		Provider: pack.ProviderManifest{
			ProviderID:     "deepseek",
			SmokeTestModel: "deepseek-chat",
		},
	})
	if err == nil || err.Error() != "latest import batch is failed; run import again before reconcile" {
		t.Fatalf("Reconcile() error = %v, want non-reconcilable batch error", err)
	}
}

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

	store := openReconcileTestStore(t)
	defer closeReconcileTestStore(t, store)

	fixture := seedReconcileFixture(t, store)
	mustCreateImportBatchItem(t, store, fixture.batchID, "fp-1", `{"account_id":"account-1"}`)
	mustCreateManagedResource(t, store, fixture.batchID, fixture.hostPK, "group", "group-1", "group one")
	mustCreateManagedResource(t, store, fixture.batchID, fixture.hostPK, "account", "account-1", "account one")
	mustCreateAndLoadAccessClosures(t, store, fixture.batchID, sqlite.AccessClosureRecord{
		BatchID:     fixture.batchID,
		ClosureType: accessModeSelfService,
		Status:      accessStatusSelfServiceReady,
	})

	host := &reconcileHostStub{
		testResults: map[string]sub2api.ProbeResult{
			"account-1": {OK: true, Status: accountStatusPassed, Message: "ok"},
		},
		models: map[string][]sub2api.AccountModel{
			"account-1": {{ID: "deepseek-chat"}},
		},
		gatewayResult: sub2api.GatewayAccessResult{
			OK:               true,
			StatusCode:       200,
			HasExpectedModel: true,
			CompletionOK:     true,
			Models:           []string{"deepseek-chat"},
		},
		completionResults: []sub2api.GatewayCompletionResult{
			{OK: true, StatusCode: 200},
		},
		managedResourceSnapshot: sub2api.ManagedResourceSnapshot{
			Groups:   []sub2api.NamedResource{{ID: "group-1", Name: "group one"}},
			Accounts: []sub2api.NamedResource{{ID: "account-1", Name: "account one"}},
		},
	}

	result, err := NewService(store, host).Reconcile(context.Background(), Request{
		HostID:            "host-1",
		HostBaseURL:       "https://sub2api.example.com",
		AccessProbeAPIKey: "user-key",
		Pack: pack.LoadedPack{
			Manifest: pack.Manifest{PackID: "openai-cn-pack", Version: "1.0.0", TargetHost: "sub2api"},
		},
		Provider: pack.ProviderManifest{
			ProviderID:     "deepseek",
			SmokeTestModel: "deepseek-chat",
		},
	})
	if err != nil {
		t.Fatalf("Reconcile() error = %v", err)
	}
	if result.Status != "active" {
		t.Fatalf("result.Status = %q, want active", result.Status)
	}
	if result.MissingCount != 0 || result.ExtraCount != 0 || result.ProbeFailureCount != 0 {
		t.Fatalf("result = %+v, want no drift and no probe failures", result)
	}
	if result.AccessStatus != accessStatusSelfServiceReady {
		t.Fatalf("result.AccessStatus = %q, want %q", result.AccessStatus, accessStatusSelfServiceReady)
	}

	runs, err := store.ReconcileRuns().GetByBatchID(context.Background(), fixture.batchID)
	if err != nil {
		t.Fatalf("ReconcileRuns().GetByBatchID() error = %v", err)
	}
	if len(runs) != 1 || runs[0].Status != "active" {
		t.Fatalf("persisted reconcile runs = %+v, want single active run", runs)
	}
}

type reconcileFixture struct {
	hostPK     int64
	packPK     int64
	providerPK int64
	batchID    int64
}

func openReconcileTestStore(t *testing.T) *sqlite.DB {
	t.Helper()
	return testutil.OpenSQLiteStore(t, testutil.SQLiteTestDSN(t, "state.db", true))
}

func closeReconcileTestStore(t *testing.T, store *sqlite.DB) {
	t.Helper()
	if err := store.Close(); err != nil {
		t.Fatalf("store.Close() error = %v", err)
	}
}

func seedReconcileFixture(t *testing.T, store *sqlite.DB) reconcileFixture {
	t.Helper()

	hostPK, err := store.Hosts().Create(context.Background(), sqlite.Host{
		HostID:      "host-1",
		BaseURL:     "https://sub2api.example.com",
		HostVersion: "0.1.126",
		AuthType:    "apikey",
		AuthToken:   "test-token",
	})
	if err != nil {
		t.Fatalf("Hosts().Create() error = %v", err)
	}
	packPK, err := store.Packs().Create(context.Background(), sqlite.Pack{
		PackID:   "openai-cn-pack",
		Version:  "1.0.0",
		Checksum: "checksum-1",
	})
	if err != nil {
		t.Fatalf("Packs().Create() error = %v", err)
	}
	providerPK, err := store.Providers().Create(context.Background(), sqlite.Provider{
		PackID:         packPK,
		ProviderID:     "deepseek",
		DisplayName:    "DeepSeek",
		BaseURL:        "https://api.example.com",
		Platform:       "openai",
		AccountType:    "openai",
		SmokeTestModel: "deepseek-chat",
	})
	if err != nil {
		t.Fatalf("Providers().Create() error = %v", err)
	}
	batchID := mustCreateImportBatch(t, store, hostPK, packPK, providerPK, "partial", "partially_succeeded", "self_service_ready")
	return reconcileFixture{
		hostPK:     hostPK,
		packPK:     packPK,
		providerPK: providerPK,
		batchID:    batchID,
	}
}

func mustCreateImportBatch(t *testing.T, store *sqlite.DB, hostPK, packPK, providerPK int64, mode, batchStatus, accessStatus string) int64 {
	t.Helper()

	id, err := store.ImportBatches().Create(context.Background(), sqlite.ImportBatch{
		HostID:       hostPK,
		PackID:       packPK,
		ProviderID:   providerPK,
		Mode:         mode,
		BatchStatus:  batchStatus,
		AccessStatus: accessStatus,
	})
	if err != nil {
		t.Fatalf("ImportBatches().Create() error = %v", err)
	}
	return id
}

func mustCreateImportBatchItem(t *testing.T, store *sqlite.DB, batchID int64, fingerprint, probeSummary string) int64 {
	t.Helper()

	id, err := store.ImportBatchItems().Create(context.Background(), sqlite.ImportBatchItem{
		BatchID:          batchID,
		KeyFingerprint:   fingerprint,
		AccountStatus:    "pending",
		ProbeSummaryJSON: probeSummary,
	})
	if err != nil {
		t.Fatalf("ImportBatchItems().Create() error = %v", err)
	}
	return id
}

func mustCreateManagedResource(t *testing.T, store *sqlite.DB, batchID, hostPK int64, resourceType, hostResourceID, resourceName string) int64 {
	t.Helper()

	id, err := store.ManagedResources().Create(context.Background(), sqlite.ManagedResource{
		BatchID:        batchID,
		HostID:         hostPK,
		ResourceType:   resourceType,
		HostResourceID: hostResourceID,
		ResourceName:   resourceName,
	})
	if err != nil {
		t.Fatalf("ManagedResources().Create() error = %v", err)
	}
	return id
}

func mustCreateAndLoadAccessClosures(t *testing.T, store *sqlite.DB, batchID int64, records ...sqlite.AccessClosureRecord) []sqlite.AccessClosureRecord {
	t.Helper()

	for _, record := range records {
		if _, err := store.AccessClosures().Create(context.Background(), record); err != nil {
			t.Fatalf("AccessClosures().Create() error = %v", err)
		}
	}
	loaded, err := store.AccessClosures().GetByBatchID(context.Background(), batchID)
	if err != nil {
		t.Fatalf("AccessClosures().GetByBatchID() error = %v", err)
	}
	return loaded
}

func mustExecReconcileSQL(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)
	}
}

type reconcileHostStub struct {
	disableResponsesErr              error
	gatewayErr                       error
	gatewayResult                    sub2api.GatewayAccessResult
	testResults                      map[string]sub2api.ProbeResult
	testErrors                       map[string]error
	models                           map[string][]sub2api.AccountModel
	modelErrors                      map[string]error
	completionResults                []sub2api.GatewayCompletionResult
	completionErrs                   []error
	completionCalls                  int
	disableResponsesCalls            int
	disabledResponsesAccounts        []string
	clearTempUnschedulableCalls      int
	clearedTempUnschedulableAccounts []string
	managedResourceSnapshot          sub2api.ManagedResourceSnapshot
	listManagedResourcesErr          error
}

func (h *reconcileHostStub) GetHostVersion(context.Context) (string, error) {
	return "0.1.126", nil
}

func (h *reconcileHostStub) ProbeCapabilities(context.Context) (sub2api.HostCapabilities, error) {
	return sub2api.HostCapabilities{}, nil
}

func (h *reconcileHostStub) CreateGroup(context.Context, sub2api.CreateGroupRequest) (sub2api.GroupRef, error) {
	return sub2api.GroupRef{}, nil
}

func (h *reconcileHostStub) DeleteGroup(context.Context, string) error {
	return nil
}

func (h *reconcileHostStub) CreateChannel(context.Context, sub2api.CreateChannelRequest) (sub2api.ChannelRef, error) {
	return sub2api.ChannelRef{}, nil
}

func (h *reconcileHostStub) UpdateChannel(context.Context, string, sub2api.CreateChannelRequest) error {
	return nil
}

func (h *reconcileHostStub) DeleteChannel(context.Context, string) error {
	return nil
}

func (h *reconcileHostStub) CreatePlan(context.Context, sub2api.CreatePlanRequest) (sub2api.PlanRef, error) {
	return sub2api.PlanRef{}, nil
}

func (h *reconcileHostStub) DeletePlan(context.Context, string) error {
	return nil
}

func (h *reconcileHostStub) CreateAccount(context.Context, sub2api.CreateAccountRequest) (sub2api.AccountRef, error) {
	return sub2api.AccountRef{}, nil
}

func (h *reconcileHostStub) BatchCreateAccounts(context.Context, sub2api.BatchCreateAccountsRequest) ([]sub2api.AccountRef, error) {
	return nil, nil
}

func (h *reconcileHostStub) DeleteAccount(context.Context, string) error {
	return nil
}

func (h *reconcileHostStub) TestAccount(_ context.Context, accountID, _ string) (sub2api.ProbeResult, error) {
	if err, ok := h.testErrors[accountID]; ok {
		return sub2api.ProbeResult{}, err
	}
	if result, ok := h.testResults[accountID]; ok {
		return result, nil
	}
	return sub2api.ProbeResult{}, fmt.Errorf("missing test result for %s", accountID)
}

func (h *reconcileHostStub) GetAccountModels(_ context.Context, accountID string) ([]sub2api.AccountModel, error) {
	if err, ok := h.modelErrors[accountID]; ok {
		return nil, err
	}
	if models, ok := h.models[accountID]; ok {
		return models, nil
	}
	return nil, fmt.Errorf("missing models for %s", accountID)
}

func (h *reconcileHostStub) EnsureSubscriptionAccess(context.Context, sub2api.EnsureSubscriptionAccessRequest) (sub2api.SubscriptionAccessRef, error) {
	return sub2api.SubscriptionAccessRef{}, nil
}

func (h *reconcileHostStub) AssignSubscription(context.Context, sub2api.AssignSubscriptionRequest) (sub2api.SubscriptionRef, error) {
	return sub2api.SubscriptionRef{}, nil
}

func (h *reconcileHostStub) CheckGatewayAccess(_ context.Context, _ sub2api.GatewayAccessCheckRequest) (sub2api.GatewayAccessResult, error) {
	if h.gatewayErr != nil {
		return sub2api.GatewayAccessResult{}, h.gatewayErr
	}
	return h.gatewayResult, nil
}

func (h *reconcileHostStub) CheckGatewayCompletion(_ context.Context, _ sub2api.GatewayCompletionCheckRequest) (sub2api.GatewayCompletionResult, error) {
	idx := h.completionCalls
	h.completionCalls++
	if idx < len(h.completionErrs) && h.completionErrs[idx] != nil {
		return sub2api.GatewayCompletionResult{}, h.completionErrs[idx]
	}
	if len(h.completionResults) == 0 {
		return sub2api.GatewayCompletionResult{}, nil
	}
	if idx >= len(h.completionResults) {
		idx = len(h.completionResults) - 1
	}
	return h.completionResults[idx], nil
}

func (h *reconcileHostStub) DisableOpenAIResponsesAPI(_ context.Context, accountIDs []string) error {
	h.disableResponsesCalls++
	h.disabledResponsesAccounts = append([]string(nil), accountIDs...)
	return h.disableResponsesErr
}

func (h *reconcileHostStub) ClearTempUnschedulable(_ context.Context, accountIDs []string) error {
	h.clearTempUnschedulableCalls++
	h.clearedTempUnschedulableAccounts = append([]string(nil), accountIDs...)
	return nil
}

func (h *reconcileHostStub) ListManagedResources(context.Context, sub2api.ListManagedResourcesRequest) (sub2api.ManagedResourceSnapshot, error) {
	if h.listManagedResourcesErr != nil {
		return sub2api.ManagedResourceSnapshot{}, h.listManagedResourcesErr
	}
	return h.managedResourceSnapshot, nil
}
