package credentialstore

import (
	"context"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"errors"
	"fmt"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	_ "modernc.org/sqlite"
)

// newTestStore opens an in-memory SQLite store for tests.
func newTestStore(t *testing.T) *Store {
	t.Helper()
	ctx := context.Background()
	s, err := Open(ctx, "file:iop_test_"+strings.ReplaceAll(t.Name(), "/", "_")+".db?mode=memory&cache=shared")
	if err != nil {
		t.Fatalf("open test store: %v", err)
	}
	t.Cleanup(func() { _ = s.Close() })
	return s
}

func TestCreatePrincipalReturnsRawTokenOnce(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "edge-prod"})
	if err != nil {
		t.Fatalf("create principal: %v", err)
	}
	if issued.Principal.Alias != "edge-prod" {
		t.Fatalf("principal alias: got %q want %q", issued.Principal.Alias, "edge-prod")
	}
	if issued.Token.Status != StatusActive {
		t.Fatalf("token status: got %q want %q", issued.Token.Status, StatusActive)
	}
	if issued.RawToken == "" {
		t.Fatal("raw token must be returned once")
	}
	if len(issued.RawToken) != 64 {
		t.Fatalf("raw token length: got %d want 64", len(issued.RawToken))
	}
	// TokenRecord must NOT contain the raw token; only digest.
	if issued.Token.Digest == issued.RawToken {
		t.Fatal("token digest must not equal raw token")
	}
	// Verify digest is SHA-256 of raw.
	sum := sha256.Sum256([]byte(issued.RawToken))
	expected := hex.EncodeToString(sum[:])
	if issued.Token.Digest != expected {
		t.Fatalf("digest mismatch: got %q want %q", issued.Token.Digest, expected)
	}
	// The raw token must not appear in any DB column.
	var storedDigest string
	if err := store.db.QueryRowContext(ctx, `SELECT digest FROM tokens WHERE principal_id=?`, issued.Principal.ID).Scan(&storedDigest); err != nil {
		t.Fatalf("query digest: %v", err)
	}
	if storedDigest != expected {
		t.Fatalf("stored digest: got %q want %q", storedDigest, expected)
	}
}

func TestCreatePrincipalRejectsDuplicateAlias(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	if _, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "dup"}); err != nil {
		t.Fatalf("first create: %v", err)
	}
	var dupErr error
	if _, dupErr = store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "dup"}); dupErr != ErrPrincipalAlreadyExists {
		t.Fatalf("expected ErrPrincipalAlreadyExists, got: %v", dupErr)
	}
	// Verify no second principal was created.
	var count int
	store.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM principals`).Scan(&count)
	if count != 1 {
		t.Fatalf("principal count: got %d want 1", count)
	}
}

func TestCreateFirstPrincipalWithTokenRejectsNonEmptyStore(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	if _, err := store.CreateFirstPrincipalWithToken(ctx, CreatePrincipalInput{Alias: "first"}); err != nil {
		t.Fatalf("first create: %v", err)
	}
	var dupErr error
	if _, dupErr = store.CreateFirstPrincipalWithToken(ctx, CreatePrincipalInput{Alias: "second"}); !errors.Is(dupErr, ErrPrincipalAlreadyExists) {
		t.Fatalf("expected ErrPrincipalAlreadyExists, got: %v", dupErr)
	}
	var count int
	store.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM principals`).Scan(&count)
	if count != 1 {
		t.Fatalf("principal count: got %d want 1", count)
	}
}

func TestCreateFirstPrincipalWithTokenAllowsExactlyOneConcurrentCaller(t *testing.T) {
	ctx := context.Background()
	tmpFile := filepath.Join(t.TempDir(), "concurrent_first.db")
	initializer, err := Open(ctx, tmpFile)
	if err != nil {
		t.Fatalf("initialize store: %v", err)
	}
	if err := initializer.Close(); err != nil {
		t.Fatalf("close initialized store: %v", err)
	}

	const numCallers = 5
	stores := openIndependentTestStores(t, ctx, tmpFile, numCallers)
	start := make(chan struct{})
	type result struct {
		issued *IssuedPrincipal
		err    error
	}
	results := make(chan result, numCallers)
	var wg sync.WaitGroup

	for i := 0; i < numCallers; i++ {
		wg.Add(1)
		alias := fmt.Sprintf("alias-%d", i)
		store := stores[i]
		go func(a string, s *Store) {
			defer wg.Done()
			<-start
			issued, err := s.CreateFirstPrincipalWithToken(ctx, CreatePrincipalInput{Alias: a})
			results <- result{issued: issued, err: err}
		}(alias, store)
	}
	close(start)
	wg.Wait()
	close(results)

	var successes, alreadyExists, tokenOutputs int
	for result := range results {
		if result.err == nil {
			successes++
			if result.issued == nil || result.issued.RawToken == "" {
				t.Fatal("successful first bootstrap must return one raw token")
			}
			tokenOutputs++
		} else if errors.Is(result.err, ErrPrincipalAlreadyExists) {
			alreadyExists++
		} else {
			t.Fatalf("unexpected error from concurrent caller: %v", result.err)
		}
	}

	if successes != 1 || tokenOutputs != 1 || alreadyExists != numCallers-1 {
		t.Fatalf("concurrent results: successes=%d tokenOutputs=%d alreadyExists=%d; want 1, 1, and %d", successes, tokenOutputs, alreadyExists, numCallers-1)
	}

	var principalCount, tokenCount int
	if err := stores[0].db.QueryRowContext(ctx, `SELECT COUNT(*) FROM principals`).Scan(&principalCount); err != nil {
		t.Fatalf("count principals: %v", err)
	}
	if err := stores[0].db.QueryRowContext(ctx, `SELECT COUNT(*) FROM tokens`).Scan(&tokenCount); err != nil {
		t.Fatalf("count tokens: %v", err)
	}
	if principalCount != 1 || tokenCount != 1 {
		t.Fatalf("persisted counts: principals=%d tokens=%d; want 1 and 1", principalCount, tokenCount)
	}
}

func openIndependentTestStores(t *testing.T, ctx context.Context, databaseURL string, count int) []*Store {
	t.Helper()
	stores := make([]*Store, 0, count)
	for range count {
		store, err := Open(ctx, databaseURL)
		if err != nil {
			for _, opened := range stores {
				_ = opened.Close()
			}
			t.Fatalf("open independent store: %v", err)
		}
		stores = append(stores, store)
	}
	t.Cleanup(func() {
		for _, store := range stores {
			_ = store.Close()
		}
	})
	return stores
}

func TestPrincipalTokenLifecycleUsesRevisionCAS(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "cas-test"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}

	// Disable with correct revision.
	_, err = store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 0)
	if err != nil {
		t.Fatalf("disable: %v", err)
	}

	// Verify revision incremented.
	var rev int64
	store.db.QueryRowContext(ctx, `SELECT revision FROM tokens WHERE principal_id=?`, issued.Principal.ID).Scan(&rev)
	if rev != 1 {
		t.Fatalf("revision after disable: got %d want 1", rev)
	}

	// A stale revision takes precedence over the inactive status.
	_, err = store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 0)
	if !errors.Is(err, ErrRevisionMismatch) {
		t.Fatalf("expected ErrRevisionMismatch, got: %v", err)
	}

	// Verify revision is still 1 (no change from failed disable).
	store.db.QueryRowContext(ctx, `SELECT revision FROM tokens WHERE principal_id=?`, issued.Principal.ID).Scan(&rev)
	if rev != 1 {
		t.Fatalf("revision after failed disable: got %d want 1", rev)
	}
}

func TestRevokeDisabledToken(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "revoke-disabled"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if _, err := store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 0); err != nil {
		t.Fatalf("disable: %v", err)
	}
	revoked, err := store.RevokeToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 1)
	if err != nil {
		t.Fatalf("revoke disabled token: %v", err)
	}
	if revoked.Status != StatusRevoked || revoked.Revision != 2 || revoked.RevokedAt == nil {
		t.Fatalf("unexpected revoked record: %+v", revoked)
	}
}

func TestRevokedTokenPersistsAcrossReopen(t *testing.T) {
	ctx := context.Background()
	databaseURL := t.TempDir() + "/revoked.db"
	store, err := Open(ctx, databaseURL)
	if err != nil {
		t.Fatalf("open: %v", err)
	}
	issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "reopen-revoked"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if _, err := store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 0); err != nil {
		t.Fatalf("disable: %v", err)
	}
	if _, err := store.RevokeToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 1); err != nil {
		t.Fatalf("revoke: %v", err)
	}
	if err := store.Close(); err != nil {
		t.Fatalf("close: %v", err)
	}

	reopened, err := Open(ctx, databaseURL)
	if err != nil {
		t.Fatalf("reopen: %v", err)
	}
	defer reopened.Close()
	if _, err := reopened.RevokeToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 2); !errors.Is(err, ErrTokenRevoked) {
		t.Fatalf("revoked state did not persist: %v", err)
	}
}

func TestTokenMutationLosingCASDoesNotSucceed(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	// Serializing the in-memory SQLite pool avoids lock errors while the two
	// callers still race for the same revision through the public API.
	store.db.SetMaxOpenConns(1)
	issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "concurrent-cas"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}

	start := make(chan struct{})
	errs := make(chan error, 2)
	var wg sync.WaitGroup
	for range 2 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			<-start
			_, err := store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 0)
			errs <- err
		}()
	}
	close(start)
	wg.Wait()
	close(errs)

	var successes, revisionMismatches int
	for err := range errs {
		if err == nil {
			successes++
			continue
		}
		if errors.Is(err, ErrRevisionMismatch) {
			revisionMismatches++
			continue
		}
		t.Fatalf("CAS loser returned an untyped error: %v", err)
	}
	if successes != 1 || revisionMismatches != 1 {
		t.Fatalf("CAS results: successes=%d revision_mismatches=%d", successes, revisionMismatches)
	}
}

func TestRevokedTokenCannotBeReenabled(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "revoke-test"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}

	// Revoke the token.
	_, err = store.RevokeToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 0)
	if err != nil {
		t.Fatalf("revoke: %v", err)
	}

	// Attempt to re-enable (disable then active) must fail.
	_, err = store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 1)
	if err != ErrTokenRevoked {
		t.Fatalf("expected ErrTokenRevoked on disable, got: %v", err)
	}

	// Attempt to re-revoke must also fail.
	_, err = store.RevokeToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 1)
	if err != ErrTokenRevoked {
		t.Fatalf("expected ErrTokenRevoked on revoke, got: %v", err)
	}

	// Verify status is revoked and revoked_at is set.
	var status string
	var revokedAt sql.NullString
	store.db.QueryRowContext(ctx, `SELECT status, revoked_at FROM tokens WHERE principal_id=?`, issued.Principal.ID).Scan(&status, &revokedAt)
	if status != StatusRevoked {
		t.Fatalf("status: got %q want %q", status, StatusRevoked)
	}
	if !revokedAt.Valid {
		t.Fatal("revoked_at must be set")
	}
}

func TestPrincipalTokenPersistsAcrossReopen(t *testing.T) {
	ctx := context.Background()
	tmpFile := t.TempDir() + "/persist.db"

	// Create and issue.
	store1, err := Open(ctx, tmpFile)
	if err != nil {
		t.Fatalf("open 1: %v", err)
	}
	issued, err := store1.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "persist"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	rawToken := issued.RawToken
	_ = store1.Close()

	// Reopen.
	store2, err := Open(ctx, tmpFile)
	if err != nil {
		t.Fatalf("open 2: %v", err)
	}
	defer store2.Close()

	// Verify principal exists.
	p, err := store2.GetPrincipal(ctx, "persist")
	if err != nil {
		t.Fatalf("get principal: %v", err)
	}
	if p == nil {
		t.Fatal("principal must persist across reopen")
	}

	// Verify token lookup by digest works after reopen.
	sum := sha256.Sum256([]byte(rawToken))
	digest := hex.EncodeToString(sum[:])
	_, token, err := store2.LookupTokenByDigest(ctx, digest)
	if err != nil {
		t.Fatalf("lookup by digest after reopen: %v", err)
	}
	if token == nil {
		t.Fatal("token must persist across reopen")
	}
	if token.Status != StatusActive {
		t.Fatalf("token status after reopen: got %q want %q", token.Status, StatusActive)
	}
}

func TestLookupTokenByDigestReturnsOnlyActive(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "active-only"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	sum := sha256.Sum256([]byte(issued.RawToken))
	digest := hex.EncodeToString(sum[:])

	// Active lookup succeeds.
	_, _, err = store.LookupTokenByDigest(ctx, digest)
	if err != nil {
		t.Fatalf("active lookup: %v", err)
	}

	// Disable and verify lookup fails.
	_, err = store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 0)
	if err != nil {
		t.Fatalf("disable: %v", err)
	}
	_, _, err = store.LookupTokenByDigest(ctx, digest)
	if err != ErrTokenNotFound {
		t.Fatalf("expected ErrTokenNotFound after disable, got: %v", err)
	}

	// Re-enable and verify lookup works again.
	// Note: DisableToken transitions active->disabled, but we need a path
	// to re-enable. For this test, we revoke and check that path.
	// Actually the test verifies disable blocks lookup; re-enable is a
	// separate concern tested elsewhere.
}

func TestListPrincipalsReturnsMetadataOnly(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "list-test"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}

	principals, err := store.ListPrincipals(ctx)
	if err != nil {
		t.Fatalf("list: %v", err)
	}
	if len(principals) != 1 {
		t.Fatalf("principal count: got %d want 1", len(principals))
	}
	p := principals[0]
	if p.Principal.Alias != "list-test" {
		t.Fatalf("alias: got %q want %q", p.Principal.Alias, "list-test")
	}
	// TokenRecord must not expose raw token.
	if p.Token.TokenRef == issued.RawToken {
		t.Fatal("list must not expose raw token in token_ref")
	}
}

func TestListPrincipalsWithoutActiveToken(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)
	issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "inactive-list"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	if _, err := store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 0); err != nil {
		t.Fatalf("disable: %v", err)
	}

	principals, err := store.ListPrincipals(ctx)
	if err != nil {
		t.Fatalf("list after disable: %v", err)
	}
	if len(principals) != 1 || principals[0].Token.TokenRef != "" || principals[0].Token.Revision != 0 {
		t.Fatalf("inactive token metadata must be zero: %+v", principals)
	}
	if _, err := store.RevokeToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 1); err != nil {
		t.Fatalf("revoke: %v", err)
	}
	principals, err = store.ListPrincipals(ctx)
	if err != nil {
		t.Fatalf("list after revoke: %v", err)
	}
	if len(principals) != 1 || principals[0].Token.TokenRef != "" {
		t.Fatalf("revoked token metadata must be zero: %+v", principals)
	}
}

func TestCreatePrincipalRequiresAlias(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	_, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{})
	if err == nil {
		t.Fatal("expected error for empty alias")
	}
}

func TestRawTokenDoesNotLeakIntoDatabase(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "leak-test"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}

	// Scan every column in both tables and verify raw token is absent.
	var principalAlias, principalID string
	store.db.QueryRowContext(ctx, `SELECT id, alias FROM principals WHERE id=?`, issued.Principal.ID).Scan(&principalID, &principalAlias)
	if principalAlias == issued.RawToken {
		t.Fatal("raw token leaked into principals.alias")
	}

	var tokenRef, digest string
	store.db.QueryRowContext(ctx, `SELECT token_ref, digest FROM tokens WHERE principal_id=?`, issued.Principal.ID).Scan(&tokenRef, &digest)
	if strings.Contains(tokenRef, issued.RawToken) {
		t.Fatal("raw token leaked into tokens.token_ref")
	}
	if strings.Contains(digest, issued.RawToken) {
		t.Fatal("raw token leaked into tokens.digest")
	}
	// Verify no column contains the raw token.
	var allText string
	rows, err := store.db.QueryContext(ctx, `SELECT * FROM tokens WHERE principal_id=?`, issued.Principal.ID)
	if err != nil {
		t.Fatalf("scan tokens: %v", err)
	}
	defer rows.Close()
	cols, _ := rows.Columns()
	for rows.Next() {
		vals := make([]interface{}, len(cols))
		ptrs := make([]interface{}, len(cols))
		for i := range vals {
			ptrs[i] = &vals[i]
		}
		if err := rows.Scan(ptrs...); err != nil {
			t.Fatalf("scan row: %v", err)
		}
		for _, v := range vals {
			if s, ok := v.(string); ok {
				allText += s
			}
		}
	}
	if strings.Contains(allText, issued.RawToken) {
		t.Fatal("raw token found in tokens table columns")
	}
}

func TestRevocationIsIrreversible(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	issued, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "irreversible"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}

	// Revoke.
	_, err = store.RevokeToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 0)
	if err != nil {
		t.Fatalf("revoke: %v", err)
	}

	// Attempt to disable (which would be a step toward re-enabling).
	_, err = store.DisableToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 1)
	if err != ErrTokenRevoked {
		t.Fatalf("expected ErrTokenRevoked, got: %v", err)
	}

	// Attempt to revoke again.
	_, err = store.RevokeToken(ctx, issued.Principal.ID, issued.Token.TokenRef, 1)
	if err != ErrTokenRevoked {
		t.Fatalf("expected ErrTokenRevoked on double revoke, got: %v", err)
	}

	// Lookup must fail (only active tokens are returned).
	sum := sha256.Sum256([]byte(issued.RawToken))
	_, _, err = store.LookupTokenByDigest(ctx, hex.EncodeToString(sum[:]))
	if err != ErrTokenNotFound {
		t.Fatalf("expected ErrTokenNotFound for revoked token, got: %v", err)
	}
}

func TestOpenReturnsNilForEmptyURL(t *testing.T) {
	ctx := context.Background()
	s, err := Open(ctx, "")
	if err != nil {
		t.Fatalf("open empty: %v", err)
	}
	if s != nil {
		t.Fatal("expected nil store for empty URL")
	}
}

func TestDialectFromURLRejectsUnknownSchemeWithDBSuffix(t *testing.T) {
	if _, err := dialectFromURL("memdb://credentials.db"); err == nil {
		t.Fatal("unknown URL scheme containing .db must not select SQLite")
	}
	for _, tc := range []struct {
		url, want string
	}{
		{"credentials.db", dialectSQLite},
		{"file:credentials.db", dialectSQLite},
		{"postgres://db/credentials", dialectPostgres},
		{"postgresql://db/credentials", dialectPostgres},
	} {
		got, err := dialectFromURL(tc.url)
		if err != nil || got != tc.want {
			t.Fatalf("dialectFromURL(%q) = %q, %v; want %q", tc.url, got, err, tc.want)
		}
	}
}

func TestBindQueryUsesPostgresOrdinals(t *testing.T) {
	query := "SELECT * FROM tokens WHERE principal_id=? AND token_ref=? AND status='active'"
	if got, want := bindQuery(dialectPostgres, query), "SELECT * FROM tokens WHERE principal_id=$1 AND token_ref=$2 AND status='active'"; got != want {
		t.Fatalf("PostgreSQL binding: got %q want %q", got, want)
	}
	if got := bindQuery(dialectSQLite, query); got != query {
		t.Fatalf("SQLite binding changed query: %q", got)
	}
}

func TestSQLiteDataSourceWithBusyTimeoutPreservesDataSourceShape(t *testing.T) {
	for _, tc := range []struct {
		name, input, want string
	}{
		{"plain path", "credentials.db", "credentials.db?_pragma=busy_timeout%3d5000"},
		{"file URL", "file:credentials.db", "file:credentials.db?_pragma=busy_timeout%3d5000"},
		{"query", "file:credentials.db?mode=memory&cache=shared", "file:credentials.db?mode=memory&cache=shared&_pragma=busy_timeout%3d5000"},
		{"memory", ":memory:", ":memory:?_pragma=busy_timeout%3d5000"},
		{"fragment", "file:credentials.db?mode=rwc#fragment", "file:credentials.db?mode=rwc&_pragma=busy_timeout%3d5000#fragment"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			if got := sqliteDataSourceWithBusyTimeout(tc.input); got != tc.want {
				t.Fatalf("sqliteDataSourceWithBusyTimeout(%q) = %q, want %q", tc.input, got, tc.want)
			}
		})
	}
}

func TestSchemaStatementsMatchDialect(t *testing.T) {
	postgres := strings.Join(schemaStatements(dialectPostgres), "\n")
	if strings.Contains(postgres, "DATETIME") || strings.Contains(postgres, "?") || !strings.Contains(postgres, "TIMESTAMPTZ") {
		t.Fatalf("PostgreSQL schema is not executable: %s", postgres)
	}
	sqlite := strings.Join(schemaStatements(dialectSQLite), "\n")
	if !strings.Contains(sqlite, "created_at TEXT") {
		t.Fatalf("SQLite schema must use text timestamps: %s", sqlite)
	}
}

func TestOpenRejectsUnsupportedScheme(t *testing.T) {
	ctx := context.Background()
	_, err := Open(ctx, "memdb://something")
	if err == nil {
		t.Fatal("expected error for unsupported scheme")
	}
}

func TestOpenRejectsUnreachablePostgres(t *testing.T) {
	ctx := context.Background()
	_, err := Open(ctx, "postgres://localhost:99999/nonexistent")
	if err == nil {
		t.Fatal("expected error for unreachable postgres")
	}
}

func TestSQLiteReopenPreservesSchema(t *testing.T) {
	ctx := context.Background()
	tmpFile := t.TempDir() + "/schema.db"

	// First open creates schema.
	s1, err := Open(ctx, tmpFile)
	if err != nil {
		t.Fatalf("open 1: %v", err)
	}
	_, err = s1.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "schema-test"})
	if err != nil {
		t.Fatalf("create: %v", err)
	}
	_ = s1.Close()

	// Second open must not re-fail on schema.
	s2, err := Open(ctx, tmpFile)
	if err != nil {
		t.Fatalf("open 2: %v", err)
	}
	defer s2.Close()

	// Data must persist.
	p, err := s2.GetPrincipal(ctx, "schema-test")
	if err != nil {
		t.Fatalf("get: %v", err)
	}
	if p == nil {
		t.Fatal("principal must persist")
	}
}

func TestAdditionalTokenIssueAndList(t *testing.T) {
	ctx := context.Background()
	store := newTestStore(t)

	gen0, err := store.ProjectionGeneration(ctx)
	if err != nil {
		t.Fatalf("ProjectionGeneration 0: %v", err)
	}

	issuedP, err := store.CreatePrincipalWithToken(ctx, CreatePrincipalInput{Alias: "add-token-principal"})
	if err != nil {
		t.Fatalf("CreatePrincipalWithToken: %v", err)
	}
	gen1, err := store.ProjectionGeneration(ctx)
	if err != nil {
		t.Fatalf("ProjectionGeneration 1: %v", err)
	}
	if gen1 != gen0+1 {
		t.Fatalf("ProjectionGeneration after CreatePrincipalWithToken: got %d want %d", gen1, gen0+1)
	}

	dig0 := digestOf(issuedP.RawToken)
	if issuedP.Token.Digest != dig0 {
		t.Fatalf("issuedP token digest mismatch: got %q want %q", issuedP.Token.Digest, dig0)
	}
	if issuedP.Token.TokenRef != tokenRefFor(dig0) {
		t.Fatalf("issuedP tokenRef mismatch: got %q want %q", issuedP.Token.TokenRef, tokenRefFor(dig0))
	}

	tok1, err := store.CreateToken(ctx, issuedP.Principal.ID)
	if err != nil {
		t.Fatalf("CreateToken 1: %v", err)
	}
	gen2, err := store.ProjectionGeneration(ctx)
	if err != nil {
		t.Fatalf("ProjectionGeneration 2: %v", err)
	}
	if gen2 != gen1+1 {
		t.Fatalf("ProjectionGeneration after CreateToken 1: got %d want %d", gen2, gen1+1)
	}
	if tok1.RawToken == "" {
		t.Fatal("RawToken must be returned on CreateToken")
	}
	if tok1.Token.PrincipalID != issuedP.Principal.ID {
		t.Fatalf("PrincipalID mismatch: got %q want %q", tok1.Token.PrincipalID, issuedP.Principal.ID)
	}
	dig1 := digestOf(tok1.RawToken)
	if tok1.Token.Digest != dig1 {
		t.Fatalf("tok1 digest mismatch: got %q want %q", tok1.Token.Digest, dig1)
	}
	if tok1.Token.TokenRef != tokenRefFor(dig1) {
		t.Fatalf("tok1 tokenRef mismatch: got %q want %q", tok1.Token.TokenRef, tokenRefFor(dig1))
	}

	tok2, err := store.CreateToken(ctx, issuedP.Principal.ID)
	if err != nil {
		t.Fatalf("CreateToken 2: %v", err)
	}
	gen3, err := store.ProjectionGeneration(ctx)
	if err != nil {
		t.Fatalf("ProjectionGeneration 3: %v", err)
	}
	if gen3 != gen2+1 {
		t.Fatalf("ProjectionGeneration after CreateToken 2: got %d want %d", gen3, gen2+1)
	}
	if tok2.RawToken == "" {
		t.Fatal("RawToken must be returned on CreateToken 2")
	}
	if tok2.Token.PrincipalID != issuedP.Principal.ID {
		t.Fatalf("PrincipalID mismatch: got %q want %q", tok2.Token.PrincipalID, issuedP.Principal.ID)
	}
	dig2 := digestOf(tok2.RawToken)
	if tok2.Token.Digest != dig2 {
		t.Fatalf("tok2 digest mismatch: got %q want %q", tok2.Token.Digest, dig2)
	}
	if tok2.Token.TokenRef != tokenRefFor(dig2) {
		t.Fatalf("tok2 tokenRef mismatch: got %q want %q", tok2.Token.TokenRef, tokenRefFor(dig2))
	}

	rows, err := store.db.QueryContext(ctx, store.bind(`SELECT id, principal_id, token_ref, digest, status, revision FROM tokens WHERE principal_id=? ORDER BY created_at ASC, id ASC`), issuedP.Principal.ID)
	if err != nil {
		t.Fatalf("query tokens: %v", err)
	}
	defer rows.Close()

	expectedTokens := []struct {
		id       string
		tokenRef string
		digest   string
		raw      string
		status   string
		revision int64
	}{
		{id: issuedP.Token.ID, tokenRef: issuedP.Token.TokenRef, digest: dig0, raw: issuedP.RawToken, status: StatusActive, revision: 0},
		{id: tok1.Token.ID, tokenRef: tok1.Token.TokenRef, digest: dig1, raw: tok1.RawToken, status: StatusActive, revision: 0},
		{id: tok2.Token.ID, tokenRef: tok2.Token.TokenRef, digest: dig2, raw: tok2.RawToken, status: StatusActive, revision: 0},
	}

	var rowCount int
	for rows.Next() {
		if rowCount >= len(expectedTokens) {
			t.Fatalf("too many persisted token rows: count > %d", len(expectedTokens))
		}
		exp := expectedTokens[rowCount]
		var id, pid, tref, dig, status string
		var rev int64
		if err := rows.Scan(&id, &pid, &tref, &dig, &status, &rev); err != nil {
			t.Fatalf("scan token row %d: %v", rowCount, err)
		}
		if id != exp.id {
			t.Errorf("row %d ID mismatch: got %q want %q", rowCount, id, exp.id)
		}
		if pid != issuedP.Principal.ID {
			t.Errorf("row %d PrincipalID mismatch: got %q want %q", rowCount, pid, issuedP.Principal.ID)
		}
		if tref != exp.tokenRef {
			t.Errorf("row %d TokenRef mismatch: got %q want %q", rowCount, tref, exp.tokenRef)
		}
		if dig != exp.digest {
			t.Errorf("row %d Digest mismatch: got %q want %q", rowCount, dig, exp.digest)
		}
		if status != exp.status {
			t.Errorf("row %d Status mismatch: got %q want %q", rowCount, status, exp.status)
		}
		if rev != exp.revision {
			t.Errorf("row %d Revision mismatch: got %d want %d", rowCount, rev, exp.revision)
		}
		if strings.Contains(dig, exp.raw) || strings.Contains(tref, exp.raw) {
			t.Errorf("row %d leaks raw token %q in digest/token_ref", rowCount, exp.raw)
		}
		rowCount++
	}
	if err := rows.Err(); err != nil {
		t.Fatalf("token rows iteration: %v", err)
	}
	if rowCount != len(expectedTokens) {
		t.Fatalf("persisted token rows count: got %d want %d", rowCount, len(expectedTokens))
	}

	list, err := store.ListTokens(ctx, issuedP.Principal.ID)
	if err != nil {
		t.Fatalf("ListTokens: %v", err)
	}
	if len(list) != 3 {
		t.Fatalf("ListTokens count: got %d want 3", len(list))
	}
	for i, exp := range expectedTokens {
		if list[i].ID != exp.id {
			t.Errorf("list[%d] ID mismatch: got %q want %q", i, list[i].ID, exp.id)
		}
		if list[i].TokenRef != exp.tokenRef {
			t.Errorf("list[%d] TokenRef mismatch: got %q want %q", i, list[i].TokenRef, exp.tokenRef)
		}
		if list[i].Digest != exp.digest {
			t.Errorf("list[%d] Digest mismatch: got %q want %q", i, list[i].Digest, exp.digest)
		}
		if list[i].Status != exp.status {
			t.Errorf("list[%d] Status mismatch: got %q want %q", i, list[i].Status, exp.status)
		}
		if list[i].Revision != exp.revision {
			t.Errorf("list[%d] Revision mismatch: got %d want %d", i, list[i].Revision, exp.revision)
		}
	}

	genBeforeFail, err := store.ProjectionGeneration(ctx)
	if err != nil {
		t.Fatalf("ProjectionGeneration before fail: %v", err)
	}
	countBeforeFail := len(list)

	_, err = store.CreateToken(ctx, "non-existent-principal-id")
	if !errors.Is(err, ErrPrincipalNotFound) {
		t.Fatalf("expected ErrPrincipalNotFound for non-existent principal, got: %v", err)
	}

	genAfterFail, err := store.ProjectionGeneration(ctx)
	if err != nil {
		t.Fatalf("ProjectionGeneration after fail: %v", err)
	}
	if genAfterFail != genBeforeFail {
		t.Fatalf("ProjectionGeneration changed on rejected CreateToken: got %d want %d", genAfterFail, genBeforeFail)
	}

	listAfterFail, err := store.ListTokens(ctx, issuedP.Principal.ID)
	if err != nil {
		t.Fatalf("ListTokens after fail: %v", err)
	}
	if len(listAfterFail) != countBeforeFail {
		t.Fatalf("ListTokens count changed on rejected CreateToken: got %d want %d", len(listAfterFail), countBeforeFail)
	}
}

// Compile-time interface check.
var _ = (*Store)(nil)

// Ensure time import is used.
var _ = time.Now
