package credentialstore

import (
	"context"
	"database/sql"
	"path/filepath"
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"
)

type fakeKeyRegistry struct {
	keys map[string]map[uint64]bool
}

func newFakeKeyRegistry() *fakeKeyRegistry {
	return &fakeKeyRegistry{
		keys: make(map[string]map[uint64]bool),
	}
}

func (f *fakeKeyRegistry) RegisterKey(keyID string, version uint64) {
	if f.keys[keyID] == nil {
		f.keys[keyID] = make(map[uint64]bool)
	}
	f.keys[keyID][version] = true
}

func (f *fakeKeyRegistry) RemoveKey(keyID string, version uint64) {
	if versions := f.keys[keyID]; versions != nil {
		delete(versions, version)
	}
}

func (f *fakeKeyRegistry) HasEnvelopeKey(_ context.Context, keyID string, keyVersion uint64) (bool, error) {
	if versions, ok := f.keys[keyID]; ok {
		return versions[keyVersion], nil
	}
	return false, nil
}

func setupTestStoreWithRegistry(t *testing.T, registry EnvelopeKeyRegistry) (*Store, *IssuedPrincipal) {
	t.Helper()
	dbPath := filepath.Join(t.TempDir(), "test_slots.db")
	ctx := context.Background()

	opts := []Option{}
	if registry != nil {
		opts = append(opts, WithEnvelopeKeyRegistry(registry))
	}

	store, err := Open(ctx, dbPath, opts...)
	require.NoError(t, err)
	t.Cleanup(func() { _ = store.Close() })

	p, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "test-principal"})
	require.NoError(t, err)

	return store, p
}

func TestCredentialSlotsAllowSameVendorMultipleSlots(t *testing.T) {
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, p := setupTestStoreWithRegistry(t, reg)
	ctx := context.Background()

	env1 := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "k1",
		KeyVersion: 1,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("cipher1"),
	}

	slot1, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "openai",
		CredentialKind: CredentialKindBearer,
		Alias:          "slot-1",
		Envelope:       env1,
	})
	require.NoError(t, err)
	require.Equal(t, "openai", slot1.Vendor)
	require.Equal(t, "slot-1", slot1.Alias)

	env2 := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "k1",
		KeyVersion: 1,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("cipher2"),
	}

	slot2, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "openai",
		CredentialKind: CredentialKindBearer,
		Alias:          "slot-2",
		Envelope:       env2,
	})
	require.NoError(t, err)
	require.Equal(t, "openai", slot2.Vendor)
	require.Equal(t, "slot-2", slot2.Alias)

	slots, err := store.ListSlots(ctx, p.Principal.ID)
	require.NoError(t, err)
	require.Len(t, slots, 2)
}

func TestCredentialSlotAcceptsCallerSuppliedSlotID(t *testing.T) {
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, p := setupTestStoreWithRegistry(t, reg)
	ctx := context.Background()
	env := SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")}

	callerID := uuid.New().String()
	created, err := store.CreateSlot(ctx, CreateSlotInput{
		SlotID: callerID, PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "caller-id", Envelope: env,
	})
	require.NoError(t, err)
	require.Equal(t, callerID, created.ID)

	// The persisted row and its initial revision both use the caller id.
	loaded, err := store.GetSlot(ctx, p.Principal.ID, callerID)
	require.NoError(t, err)
	require.Equal(t, callerID, loaded.ID)
	revs, err := store.ListSlotRevisions(ctx, p.Principal.ID, callerID)
	require.NoError(t, err)
	require.Len(t, revs, 1)
	require.Equal(t, callerID, revs[0].SlotID)

	// A non-UUID caller id is rejected before any row is written.
	_, err = store.CreateSlot(ctx, CreateSlotInput{
		SlotID: "not-a-uuid", PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "bad-id", Envelope: env,
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "slot_id")

	// The legacy zero value still yields a store-generated id.
	legacy, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "legacy-id", Envelope: env,
	})
	require.NoError(t, err)
	require.NotEmpty(t, legacy.ID)
	require.NotEqual(t, callerID, legacy.ID)
}

func TestCredentialSlotRejectsUnknownEnvelopeKey(t *testing.T) {
	reg := newFakeKeyRegistry()
	// Do not register "unknown-key"
	store, p := setupTestStoreWithRegistry(t, reg)
	ctx := context.Background()

	env := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "unknown-key",
		KeyVersion: 1,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("cipher"),
	}

	_, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "openai",
		CredentialKind: CredentialKindBearer,
		Alias:          "slot-unknown",
		Envelope:       env,
	})
	require.Error(t, err)
	require.ErrorIs(t, err, ErrUnknownEnvelopeKey)

	// Test unconfigured store (unavailable key registry)
	unconfiguredStore, p2 := setupTestStoreWithRegistry(t, nil)
	envRegistered := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "k1",
		KeyVersion: 1,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("cipher"),
	}
	_, err = unconfiguredStore.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p2.Principal.ID,
		Vendor:         "openai",
		CredentialKind: CredentialKindBearer,
		Alias:          "slot-unavail",
		Envelope:       envRegistered,
	})
	require.Error(t, err)
	require.ErrorIs(t, err, ErrEnvelopeKeyUnavailable)
}

func TestCredentialSlotRotationRequiresRegisteredKey(t *testing.T) {
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, p := setupTestStoreWithRegistry(t, reg)
	ctx := context.Background()

	env1 := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "k1",
		KeyVersion: 1,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("cipher1"),
	}

	slot, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "openai",
		CredentialKind: CredentialKindBearer,
		Alias:          "rotate-slot",
		Envelope:       env1,
	})
	require.NoError(t, err)

	// Attempt rotation with unregistered key "k2"
	env2 := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "k2",
		KeyVersion: 1,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("cipher2"),
	}

	_, err = store.RotateSlotSecret(ctx, RotateSlotSecretInput{
		PrincipalID:     p.Principal.ID,
		SlotID:          slot.ID,
		CurrentRevision: slot.Revision,
		Envelope:        env2,
	})
	require.Error(t, err)
	require.ErrorIs(t, err, ErrUnknownEnvelopeKey)

	// Register k2 and rotate successfully
	reg.RegisterKey("k2", 1)
	rotated, err := store.RotateSlotSecret(ctx, RotateSlotSecretInput{
		PrincipalID:     p.Principal.ID,
		SlotID:          slot.ID,
		CurrentRevision: slot.Revision,
		Envelope:        env2,
	})
	require.NoError(t, err)
	require.Equal(t, int64(1), rotated.Revision)
	require.Equal(t, "k2", rotated.Envelope.KeyID)
}

func TestCredentialSlotAliasUniqueWithinPrincipal(t *testing.T) {
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, p := setupTestStoreWithRegistry(t, reg)
	ctx := context.Background()

	env := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "k1",
		KeyVersion: 1,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("cipher1"),
	}

	_, err := store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "openai",
		CredentialKind: CredentialKindBearer,
		Alias:          "dup-alias",
		Envelope:       env,
	})
	require.NoError(t, err)

	_, err = store.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "anthropic",
		CredentialKind: CredentialKindAPIKey,
		Alias:          "dup-alias",
		Envelope:       env,
	})
	require.Error(t, err)
	require.ErrorIs(t, err, ErrSlotAliasAlreadyExists)
}

func TestCredentialSlotLifecyclePersists(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "lifecycle.db")
	ctx := context.Background()

	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	reg.RegisterKey("k1", 2)

	store1, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg))
	require.NoError(t, err)

	p, err := store1.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "life-principal"})
	require.NoError(t, err)

	env1 := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "k1",
		KeyVersion: 1,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("cipher-v1"),
	}

	slot, err := store1.CreateSlot(ctx, CreateSlotInput{
		PrincipalID:    p.Principal.ID,
		Vendor:         "openai",
		CredentialKind: CredentialKindBearer,
		Alias:          "life-slot",
		Envelope:       env1,
	})
	require.NoError(t, err)
	require.Equal(t, StatusDraft, slot.Status)

	activatedRoute, err := store1.CreateRoute(ctx, CreateRouteInput{
		PrincipalID:   p.Principal.ID,
		SlotID:        slot.ID,
		Alias:         "life-route",
		ProfileID:     "openai",
		UpstreamModel: "gpt-4o",
	})
	require.NoError(t, err)
	_ = activatedRoute
	slot, err = store1.GetSlot(ctx, p.Principal.ID, slot.ID)
	require.NoError(t, err)
	require.Equal(t, StatusActive, slot.Status)

	// Disable slot
	disabled, err := store1.DisableSlot(ctx, p.Principal.ID, slot.ID, slot.Revision)
	require.NoError(t, err)
	require.Equal(t, StatusDisabled, disabled.Status)
	require.Equal(t, int64(2), disabled.Revision)

	// Enable slot
	enabled, err := store1.EnableSlot(ctx, p.Principal.ID, slot.ID, disabled.Revision)
	require.NoError(t, err)
	require.Equal(t, StatusActive, enabled.Status)
	require.Equal(t, int64(3), enabled.Revision)

	// Rotate secret
	env2 := SecretEnvelope{
		Algorithm:  "AES-256-GCM",
		KeyID:      "k1",
		KeyVersion: 2,
		Nonce:      []byte("123456789012"),
		Ciphertext: []byte("cipher-v2"),
	}
	rotated, err := store1.RotateSlotSecret(ctx, RotateSlotSecretInput{
		PrincipalID:     p.Principal.ID,
		SlotID:          slot.ID,
		CurrentRevision: enabled.Revision,
		Envelope:        env2,
	})
	require.NoError(t, err)
	require.Equal(t, int64(4), rotated.Revision)
	require.Equal(t, uint64(2), rotated.Envelope.KeyVersion)

	// Close store and reopen from dbPath
	require.NoError(t, store1.Close())

	store2, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg))
	require.NoError(t, err)
	defer store2.Close()

	reloaded, err := store2.GetSlot(ctx, p.Principal.ID, slot.ID)
	require.NoError(t, err)
	require.Equal(t, StatusActive, reloaded.Status)
	require.Equal(t, int64(4), reloaded.Revision)
	require.Equal(t, []byte("cipher-v2"), reloaded.Envelope.Ciphertext)

	revisions, err := store2.ListSlotRevisions(ctx, p.Principal.ID, slot.ID)
	require.NoError(t, err)
	require.Len(t, revisions, 2)
	require.Equal(t, int64(0), revisions[0].Revision)
	require.Equal(t, int64(4), revisions[1].Revision)

	// Revoke slot
	revoked, err := store2.RevokeSlot(ctx, p.Principal.ID, slot.ID, reloaded.Revision)
	require.NoError(t, err)
	require.Equal(t, StatusRevoked, revoked.Status)
	require.NotNil(t, revoked.RevokedAt)
}

func TestCredentialSlotLifecycleReopensWithInactiveBindings(t *testing.T) {
	ctx := context.Background()

	for _, tc := range []struct {
		name       string
		transition func(*Store, string, string, int64) (*CredentialSlotRecord, error)
	}{
		{
			name: "disabled",
			transition: func(store *Store, principalID, slotID string, revision int64) (*CredentialSlotRecord, error) {
				return store.DisableSlot(ctx, principalID, slotID, revision)
			},
		},
		{
			name: "revoked",
			transition: func(store *Store, principalID, slotID string, revision int64) (*CredentialSlotRecord, error) {
				return store.RevokeSlot(ctx, principalID, slotID, revision)
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			dbPath := filepath.Join(t.TempDir(), tc.name+".db")
			reg := newFakeKeyRegistry()
			reg.RegisterKey("k1", 1)

			store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg))
			require.NoError(t, err)
			principal, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: tc.name + "-principal"})
			require.NoError(t, err)
			slot, err := store.CreateSlot(ctx, CreateSlotInput{
				PrincipalID: principal.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: tc.name + "-slot",
				Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")},
			})
			require.NoError(t, err)
			route, err := store.CreateRoute(ctx, CreateRouteInput{
				PrincipalID: principal.Principal.ID, SlotID: slot.ID, Alias: tc.name + "-route", ProfileID: "openai", UpstreamModel: "gpt-4o",
			})
			require.NoError(t, err)
			slot, err = store.GetSlot(ctx, principal.Principal.ID, slot.ID)
			require.NoError(t, err)

			inactive, err := tc.transition(store, principal.Principal.ID, slot.ID, slot.Revision)
			require.NoError(t, err)
			require.NoError(t, store.Close())

			reopened, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg))
			require.NoError(t, err)
			defer reopened.Close()

			reloaded, err := reopened.GetSlot(ctx, principal.Principal.ID, slot.ID)
			require.NoError(t, err)
			require.Equal(t, inactive.Status, reloaded.Status)
			retainedRoute, err := reopened.GetRoute(ctx, principal.Principal.ID, route.ID)
			require.NoError(t, err)
			require.Equal(t, StatusActive, retainedRoute.Status)

			if tc.name == "disabled" {
				enabled, err := reopened.EnableSlot(ctx, principal.Principal.ID, slot.ID, reloaded.Revision)
				require.NoError(t, err)
				require.Equal(t, StatusActive, enabled.Status)
			} else {
				_, err := reopened.EnableSlot(ctx, principal.Principal.ID, slot.ID, reloaded.Revision)
				require.Error(t, err)
			}
		})
	}
}

func TestCredentialSlotOptionalAliasDoesNotCollide(t *testing.T) {
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, p := setupTestStoreWithRegistry(t, reg)
	ctx := context.Background()
	env := SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")}

	first, err := store.CreateSlot(ctx, CreateSlotInput{PrincipalID: p.Principal.ID, Vendor: " OPENAI ", CredentialKind: " BEARER ", Envelope: env})
	require.NoError(t, err)
	require.Empty(t, first.Alias)
	require.Equal(t, "openai", first.Vendor)
	require.Equal(t, CredentialKindBearer, first.CredentialKind)

	second, err := store.CreateSlot(ctx, CreateSlotInput{PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "   ", Envelope: env})
	require.NoError(t, err)
	require.Empty(t, second.Alias)

	aliased, err := store.CreateSlot(ctx, CreateSlotInput{PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: " primary ", Envelope: env})
	require.NoError(t, err)
	require.Equal(t, "primary", aliased.Alias)
	byAlias, err := store.GetSlotByAlias(ctx, p.Principal.ID, " primary ")
	require.NoError(t, err)
	require.Equal(t, aliased.ID, byAlias.ID)

	_, err = store.CreateSlot(ctx, CreateSlotInput{PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "primary", Envelope: env})
	require.ErrorIs(t, err, ErrSlotAliasAlreadyExists)
}

func TestCredentialSlotRequiresKnownCredentialKind(t *testing.T) {
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, p := setupTestStoreWithRegistry(t, reg)
	_, err := store.CreateSlot(context.Background(), CreateSlotInput{
		PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: "session_cookie", Alias: "bad-kind",
		Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")},
	})
	require.Error(t, err)
	require.Contains(t, err.Error(), "credential_kind")
}

func TestCredentialSlotActivationRequiresRegisteredKey(t *testing.T) {
	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, p := setupTestStoreWithRegistry(t, reg)
	ctx := context.Background()
	env := SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("opaque")}
	slot, err := store.CreateSlot(ctx, CreateSlotInput{PrincipalID: p.Principal.ID, Vendor: "openai", CredentialKind: CredentialKindBearer, Alias: "key-gated", Envelope: env})
	require.NoError(t, err)
	_, err = store.CreateRoute(ctx, CreateRouteInput{PrincipalID: p.Principal.ID, SlotID: slot.ID, ProfileID: "openai", UpstreamModel: "gpt-4o"})
	require.NoError(t, err)
	slot, err = store.GetSlot(ctx, p.Principal.ID, slot.ID)
	require.NoError(t, err)
	disabled, err := store.DisableSlot(ctx, p.Principal.ID, slot.ID, slot.Revision)
	require.NoError(t, err)

	reg.RemoveKey("k1", 1)
	_, err = store.EnableSlot(ctx, p.Principal.ID, slot.ID, disabled.Revision)
	require.ErrorIs(t, err, ErrUnknownEnvelopeKey)
	reg.RegisterKey("k1", 1)
	enabled, err := store.EnableSlot(ctx, p.Principal.ID, slot.ID, disabled.Revision)
	require.NoError(t, err)
	require.Equal(t, StatusActive, enabled.Status)
}

func TestCredentialSlotMigrationPreservesIntermediateCatalog(t *testing.T) {
	ctx := context.Background()
	dbPath := filepath.Join(t.TempDir(), "intermediate.db")
	db, err := sql.Open(dialectSQLite, dbPath)
	require.NoError(t, err)
	_, err = db.Exec(`
CREATE TABLE principals (id TEXT PRIMARY KEY, alias TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE credential_slots (
 id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, vendor TEXT NOT NULL, alias TEXT NOT NULL,
 status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL,
 algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB,
 created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT, UNIQUE(principal_id, alias), UNIQUE(principal_id, id));
CREATE TABLE credential_slot_revisions (id TEXT PRIMARY KEY, slot_id TEXT NOT NULL, revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, UNIQUE(slot_id, revision));
CREATE TABLE routes (id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, slot_id TEXT NOT NULL, alias TEXT, profile_id TEXT NOT NULL, upstream_model TEXT NOT NULL, resource_selector TEXT NOT NULL DEFAULT 'default', status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT);
INSERT INTO principals VALUES ('principal-1', 'legacy', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z');
INSERT INTO credential_slots VALUES ('slot-1', 'principal-1', 'openai', 'legacy-slot', 'active', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);
INSERT INTO credential_slots VALUES ('slot-2', 'principal-1', 'anthropic', 'legacy-anthropic', 'active', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);
INSERT INTO credential_slot_revisions VALUES ('revision-1', 'slot-1', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z');
INSERT INTO credential_slot_revisions VALUES ('revision-2', 'slot-2', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z');
INSERT INTO routes VALUES ('route-1', 'principal-1', 'slot-1', 'legacy-openai', 'openai', 'gpt-4o', 'default', 'active', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);
INSERT INTO routes VALUES ('route-2', 'principal-1', 'slot-2', 'legacy-anthropic-route', 'anthropic', 'claude-3-5-sonnet', 'default', 'active', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);`)
	require.NoError(t, err)
	require.NoError(t, db.Close())

	reg := newFakeKeyRegistry()
	reg.RegisterKey("k1", 1)
	store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg))
	require.NoError(t, err)
	defer store.Close()
	legacy, err := store.GetSlot(ctx, "principal-1", "slot-1")
	require.NoError(t, err)
	require.Equal(t, CredentialKindBearer, legacy.CredentialKind)
	require.Equal(t, "legacy-slot", legacy.Alias)
	legacyAnthropic, err := store.GetSlot(ctx, "principal-1", "slot-2")
	require.NoError(t, err)
	require.Equal(t, CredentialKindAPIKey, legacyAnthropic.CredentialKind)

	created, err := store.CreateSlot(ctx, CreateSlotInput{PrincipalID: "principal-1", Vendor: "openai", CredentialKind: CredentialKindBearer, Envelope: SecretEnvelope{Algorithm: "AES-256-GCM", KeyID: "k1", KeyVersion: 1, Nonce: []byte("123456789012"), Ciphertext: []byte("new")}})
	require.NoError(t, err)
	require.Empty(t, created.Alias)
	require.NoError(t, store.Close())

	reopened, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(reg))
	require.NoError(t, err)
	defer reopened.Close()
	loaded, err := reopened.GetSlot(ctx, "principal-1", created.ID)
	require.NoError(t, err)
	require.Empty(t, loaded.Alias)
}

func TestCredentialSlotMigrationRejectsIncompatibleActiveBinding(t *testing.T) {
	ctx := context.Background()
	dbPath := filepath.Join(t.TempDir(), "incompatible-intermediate.db")
	db, err := sql.Open(dialectSQLite, dbPath)
	require.NoError(t, err)
	_, err = db.Exec(`
CREATE TABLE principals (id TEXT PRIMARY KEY, alias TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE credential_slots (
 id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, vendor TEXT NOT NULL, alias TEXT NOT NULL,
 status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL,
 algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB,
 created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT, UNIQUE(principal_id, alias), UNIQUE(principal_id, id));
CREATE TABLE credential_slot_revisions (id TEXT PRIMARY KEY, slot_id TEXT NOT NULL, revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, UNIQUE(slot_id, revision));
CREATE TABLE routes (id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, slot_id TEXT NOT NULL, alias TEXT, profile_id TEXT NOT NULL, upstream_model TEXT NOT NULL, resource_selector TEXT NOT NULL DEFAULT 'default', status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT);
INSERT INTO principals VALUES ('principal-1', 'legacy', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z');
INSERT INTO credential_slots VALUES ('slot-1', 'principal-1', 'unknown-provider', 'legacy-slot', 'active', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);
INSERT INTO credential_slot_revisions VALUES ('revision-1', 'slot-1', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z');
INSERT INTO routes VALUES ('route-1', 'principal-1', 'slot-1', 'legacy-route', 'openai', 'model', 'default', 'active', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);`)
	require.NoError(t, err)
	require.NoError(t, db.Close())

	store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(newFakeKeyRegistry()))
	require.Nil(t, store)
	require.ErrorIs(t, err, ErrIncompatibleProfile)
	require.NotContains(t, err.Error(), "opaque")
}

func TestCredentialSlotMigrationRejectsIncompatibleInactiveBinding(t *testing.T) {
	ctx := context.Background()
	dbPath := filepath.Join(t.TempDir(), "incompatible-inactive-intermediate.db")
	db, err := sql.Open(dialectSQLite, dbPath)
	require.NoError(t, err)
	_, err = db.Exec(`
CREATE TABLE principals (id TEXT PRIMARY KEY, alias TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE credential_slots (
 id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, vendor TEXT NOT NULL, alias TEXT NOT NULL,
 status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL,
 algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB,
 created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT, UNIQUE(principal_id, alias), UNIQUE(principal_id, id));
CREATE TABLE credential_slot_revisions (id TEXT PRIMARY KEY, slot_id TEXT NOT NULL, revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, UNIQUE(slot_id, revision));
CREATE TABLE routes (id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, slot_id TEXT NOT NULL, alias TEXT, profile_id TEXT NOT NULL, upstream_model TEXT NOT NULL, resource_selector TEXT NOT NULL DEFAULT 'default', status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT);
INSERT INTO principals VALUES ('principal-1', 'legacy', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z');
INSERT INTO credential_slots VALUES ('slot-1', 'principal-1', 'unknown-provider', 'legacy-slot', 'disabled', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);
INSERT INTO credential_slot_revisions VALUES ('revision-1', 'slot-1', 0, 'AES-256-GCM', 'k1', 1, X'313233343536373839303132', X'6F7061717565', NULL, '2026-01-01T00:00:00Z');
INSERT INTO routes VALUES ('route-1', 'principal-1', 'slot-1', 'legacy-route', 'openai', 'model', 'default', 'active', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);`)
	require.NoError(t, err)
	require.NoError(t, db.Close())

	store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(newFakeKeyRegistry()))
	require.Nil(t, store)
	require.ErrorIs(t, err, ErrIncompatibleProfile)
	require.NotContains(t, err.Error(), "opaque")
}

func TestCredentialSlotMigrationRejectsActiveRouteWithoutSlot(t *testing.T) {
	ctx := context.Background()
	dbPath := filepath.Join(t.TempDir(), "orphan-route-intermediate.db")
	db, err := sql.Open(dialectSQLite, dbPath)
	require.NoError(t, err)
	_, err = db.Exec(`
CREATE TABLE principals (id TEXT PRIMARY KEY, alias TEXT NOT NULL UNIQUE, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
CREATE TABLE credential_slots (
 id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, vendor TEXT NOT NULL, alias TEXT NOT NULL,
 status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL,
 algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB,
 created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT, UNIQUE(principal_id, alias), UNIQUE(principal_id, id));
CREATE TABLE credential_slot_revisions (id TEXT PRIMARY KEY, slot_id TEXT NOT NULL, revision INTEGER NOT NULL, algorithm TEXT NOT NULL, key_id TEXT NOT NULL, key_version INTEGER NOT NULL, nonce BLOB NOT NULL, ciphertext BLOB NOT NULL, aad BLOB, created_at TEXT NOT NULL, UNIQUE(slot_id, revision));
CREATE TABLE routes (id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, slot_id TEXT NOT NULL, alias TEXT, profile_id TEXT NOT NULL, upstream_model TEXT NOT NULL, resource_selector TEXT NOT NULL DEFAULT 'default', status TEXT NOT NULL CHECK(status IN ('active','disabled','revoked')), revision INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, revoked_at TEXT);
INSERT INTO principals VALUES ('principal-1', 'legacy', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z');
INSERT INTO routes VALUES ('route-1', 'principal-1', 'nonexistent-slot', 'legacy-route', 'openai', 'model', 'default', 'active', 0, '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z', NULL);`)
	require.NoError(t, err)
	require.NoError(t, db.Close())

	store, err := Open(ctx, dbPath, WithEnvelopeKeyRegistry(newFakeKeyRegistry()))
	require.Nil(t, store)
	require.ErrorIs(t, err, ErrIncompatibleProfile)
	require.NotContains(t, err.Error(), "opaque")
}
